From 6aa9bd0f77dcb5128167fae62e32aa5252fe85c6 Mon Sep 17 00:00:00 2001 From: Petr Mrázek Date: Mon, 2 Dec 2013 00:55:24 +0100 Subject: Renew the updater branch Now with some actual consensus on what the updater will do! --- mmc_updater/src/AppInfo.cpp | 23 ++ mmc_updater/src/AppInfo.h | 39 ++ mmc_updater/src/CMakeLists.txt | 121 ++++++ mmc_updater/src/DirIterator.cpp | 85 ++++ mmc_updater/src/DirIterator.h | 43 +++ mmc_updater/src/FileUtils.cpp | 557 +++++++++++++++++++++++++++ mmc_updater/src/FileUtils.h | 141 +++++++ mmc_updater/src/Log.cpp | 65 ++++ mmc_updater/src/Log.h | 46 +++ mmc_updater/src/MacBundle.cpp | 53 +++ mmc_updater/src/MacBundle.h | 35 ++ mmc_updater/src/Platform.h | 30 ++ mmc_updater/src/ProcessUtils.cpp | 536 ++++++++++++++++++++++++++ mmc_updater/src/ProcessUtils.h | 97 +++++ mmc_updater/src/StandardDirs.cpp | 63 +++ mmc_updater/src/StandardDirs.h | 22 ++ mmc_updater/src/StandardDirs.mm | 18 + mmc_updater/src/StlSymbolsLeopard.cpp | 75 ++++ mmc_updater/src/StringUtils.h | 46 +++ mmc_updater/src/UpdateDialog.cpp | 25 ++ mmc_updater/src/UpdateDialog.h | 29 ++ mmc_updater/src/UpdateDialogAscii.cpp | 70 ++++ mmc_updater/src/UpdateDialogAscii.h | 32 ++ mmc_updater/src/UpdateDialogCocoa.h | 32 ++ mmc_updater/src/UpdateDialogCocoa.mm | 194 ++++++++++ mmc_updater/src/UpdateDialogGtk.cpp | 155 ++++++++ mmc_updater/src/UpdateDialogGtk.h | 42 ++ mmc_updater/src/UpdateDialogGtkFactory.cpp | 59 +++ mmc_updater/src/UpdateDialogGtkFactory.h | 13 + mmc_updater/src/UpdateDialogWin32.cpp | 215 +++++++++++ mmc_updater/src/UpdateDialogWin32.h | 39 ++ mmc_updater/src/UpdateInstaller.cpp | 439 +++++++++++++++++++++ mmc_updater/src/UpdateInstaller.h | 70 ++++ mmc_updater/src/UpdateMessage.h | 42 ++ mmc_updater/src/UpdateObserver.h | 15 + mmc_updater/src/UpdateScript.cpp | 98 +++++ mmc_updater/src/UpdateScript.h | 86 +++++ mmc_updater/src/UpdaterOptions.cpp | 156 ++++++++ mmc_updater/src/UpdaterOptions.h | 27 ++ mmc_updater/src/main.cpp | 201 ++++++++++ mmc_updater/src/resources/Info.plist | 38 ++ mmc_updater/src/resources/icon128.png | Bin 0 -> 3802 bytes mmc_updater/src/resources/icon64.png | Bin 0 -> 2182 bytes mmc_updater/src/resources/mac.icns | Bin 0 -> 43606 bytes mmc_updater/src/resources/updater.ico | Bin 0 -> 82726 bytes mmc_updater/src/resources/updater.rc | 30 ++ mmc_updater/src/tests/CMakeLists.txt | 51 +++ mmc_updater/src/tests/TestFileUtils.cpp | 50 +++ mmc_updater/src/tests/TestFileUtils.h | 10 + mmc_updater/src/tests/TestUpdateScript.cpp | 48 +++ mmc_updater/src/tests/TestUpdateScript.h | 9 + mmc_updater/src/tests/TestUpdaterOptions.cpp | 68 ++++ mmc_updater/src/tests/TestUpdaterOptions.h | 8 + mmc_updater/src/tests/TestUtils.h | 108 ++++++ mmc_updater/src/tests/file_list.xml | 52 +++ mmc_updater/src/tests/new_app.cpp | 8 + mmc_updater/src/tests/old_app.cpp | 7 + mmc_updater/src/tests/test-update.rb | 218 +++++++++++ mmc_updater/src/tests/v2_file_list.xml | 67 ++++ 59 files changed, 4906 insertions(+) create mode 100644 mmc_updater/src/AppInfo.cpp create mode 100644 mmc_updater/src/AppInfo.h create mode 100644 mmc_updater/src/CMakeLists.txt create mode 100644 mmc_updater/src/DirIterator.cpp create mode 100644 mmc_updater/src/DirIterator.h create mode 100644 mmc_updater/src/FileUtils.cpp create mode 100644 mmc_updater/src/FileUtils.h create mode 100644 mmc_updater/src/Log.cpp create mode 100644 mmc_updater/src/Log.h create mode 100644 mmc_updater/src/MacBundle.cpp create mode 100644 mmc_updater/src/MacBundle.h create mode 100644 mmc_updater/src/Platform.h create mode 100644 mmc_updater/src/ProcessUtils.cpp create mode 100644 mmc_updater/src/ProcessUtils.h create mode 100644 mmc_updater/src/StandardDirs.cpp create mode 100644 mmc_updater/src/StandardDirs.h create mode 100644 mmc_updater/src/StandardDirs.mm create mode 100644 mmc_updater/src/StlSymbolsLeopard.cpp create mode 100644 mmc_updater/src/StringUtils.h create mode 100644 mmc_updater/src/UpdateDialog.cpp create mode 100644 mmc_updater/src/UpdateDialog.h create mode 100644 mmc_updater/src/UpdateDialogAscii.cpp create mode 100644 mmc_updater/src/UpdateDialogAscii.h create mode 100644 mmc_updater/src/UpdateDialogCocoa.h create mode 100644 mmc_updater/src/UpdateDialogCocoa.mm create mode 100644 mmc_updater/src/UpdateDialogGtk.cpp create mode 100644 mmc_updater/src/UpdateDialogGtk.h create mode 100644 mmc_updater/src/UpdateDialogGtkFactory.cpp create mode 100644 mmc_updater/src/UpdateDialogGtkFactory.h create mode 100644 mmc_updater/src/UpdateDialogWin32.cpp create mode 100644 mmc_updater/src/UpdateDialogWin32.h create mode 100644 mmc_updater/src/UpdateInstaller.cpp create mode 100644 mmc_updater/src/UpdateInstaller.h create mode 100644 mmc_updater/src/UpdateMessage.h create mode 100644 mmc_updater/src/UpdateObserver.h create mode 100644 mmc_updater/src/UpdateScript.cpp create mode 100644 mmc_updater/src/UpdateScript.h create mode 100644 mmc_updater/src/UpdaterOptions.cpp create mode 100644 mmc_updater/src/UpdaterOptions.h create mode 100644 mmc_updater/src/main.cpp create mode 100644 mmc_updater/src/resources/Info.plist create mode 100644 mmc_updater/src/resources/icon128.png create mode 100644 mmc_updater/src/resources/icon64.png create mode 100644 mmc_updater/src/resources/mac.icns create mode 100644 mmc_updater/src/resources/updater.ico create mode 100644 mmc_updater/src/resources/updater.rc create mode 100644 mmc_updater/src/tests/CMakeLists.txt create mode 100644 mmc_updater/src/tests/TestFileUtils.cpp create mode 100644 mmc_updater/src/tests/TestFileUtils.h create mode 100644 mmc_updater/src/tests/TestUpdateScript.cpp create mode 100644 mmc_updater/src/tests/TestUpdateScript.h create mode 100644 mmc_updater/src/tests/TestUpdaterOptions.cpp create mode 100644 mmc_updater/src/tests/TestUpdaterOptions.h create mode 100644 mmc_updater/src/tests/TestUtils.h create mode 100644 mmc_updater/src/tests/file_list.xml create mode 100644 mmc_updater/src/tests/new_app.cpp create mode 100644 mmc_updater/src/tests/old_app.cpp create mode 100755 mmc_updater/src/tests/test-update.rb create mode 100644 mmc_updater/src/tests/v2_file_list.xml (limited to 'mmc_updater/src') 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 + +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 + +/** 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 +#endif + +#include + +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 + +#ifdef PLATFORM_UNIX +#include +#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 +#include +#include +#include +#include + +#ifdef PLATFORM_UNIX +#include +#include +#include +#include +#include +#include +#include +#include +#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)) != 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(inputFile.tellg())); + inputFile.seekg(0, std::ios::beg); + inputFile.read(&content[0], static_cast(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 +#include + +#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 +#include + +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 +#include + +#include +#include + +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(infoPlist.size())); + + // save the icon to Contents/Resources/.icns + FileUtils::writeFile((resourceDir + '/' + m_appName + ".icns").c_str(),icon.c_str(),static_cast(icon.size())); + + // copy the app binary to Contents/MacOS/ + 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 + +/** Class for creating minimal Mac app bundles. */ +class MacBundle +{ + public: + /** Create a MacBundle instance representing the bundle + * in /.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 + + // 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 +#include +#include + +#ifdef PLATFORM_WINDOWS +#include +#else +#include +#include +#include +#include +#endif + +#ifdef PLATFORM_MAC +#include +#include +#endif + +PLATFORM_PID ProcessUtils::currentProcessId() +{ +#ifdef PLATFORM_UNIX + return getpid(); +#else + return GetCurrentProcessId(); +#endif +} + +int ProcessUtils::runSync(const std::string& executable, + const std::list& 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& args) +{ + PLATFORM_PID pid = runAsyncUnix(executable,args); + int status = 0; + if (waitpid(pid,&status,0) != -1) + { + if (WIFEXITED(status)) + { + return static_cast(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& 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& 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& 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 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 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& 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::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& arguments) +{ + std::string quotedArgs; + for (std::list::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& 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& 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::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& _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 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 +#include + +/** 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& args); + + /** Start a process and return without waiting for + * it to finish. + */ + static void runAsync(const std::string& executable, + const std::list& 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& 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& args, + const std::string& task); + static int runElevatedMac(const std::string& executable, + const std::list& args); + static int runElevatedWindows(const std::string& executable, + const std::list& args); + + static PLATFORM_PID runAsyncUnix(const std::string& executable, + const std::list& args); + static int runWindows(const std::string& executable, + const std::list& args, + RunMode runMode); + static int runSyncUnix(const std::string& executable, + const std::list& 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 + #include + #include +#endif + +#ifdef PLATFORM_WINDOWS +#include +#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 + +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 + +#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 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 + +_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 +#include +#include +#include + +template +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; + + vi