Renew the updater branch
Now with some actual consensus on what the updater will do!
This commit is contained in:
23
mmc_updater/src/AppInfo.cpp
Normal file
23
mmc_updater/src/AppInfo.cpp
Normal file
@ -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;
|
||||
}
|
||||
|
39
mmc_updater/src/AppInfo.h
Normal file
39
mmc_updater/src/AppInfo.h
Normal file
@ -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";
|
||||
}
|
||||
|
121
mmc_updater/src/CMakeLists.txt
Normal file
121
mmc_updater/src/CMakeLists.txt
Normal file
@ -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()
|
||||
|
85
mmc_updater/src/DirIterator.cpp
Normal file
85
mmc_updater/src/DirIterator.cpp
Normal file
@ -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
|
||||
}
|
||||
|
43
mmc_updater/src/DirIterator.h
Normal file
43
mmc_updater/src/DirIterator.h
Normal file
@ -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
|
||||
};
|
||||
|
557
mmc_updater/src/FileUtils.cpp
Normal file
557
mmc_updater/src/FileUtils.cpp
Normal file
@ -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
|
||||
}
|
||||
|
141
mmc_updater/src/FileUtils.h
Normal file
141
mmc_updater/src/FileUtils.h
Normal file
@ -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);
|
||||
};
|
||||
|
65
mmc_updater/src/Log.cpp
Normal file
65
mmc_updater/src/Log.cpp
Normal file
@ -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();
|
||||
}
|
||||
|
46
mmc_updater/src/Log.h
Normal file
46
mmc_updater/src/Log.h
Normal file
@ -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)
|
||||
|
||||
|
53
mmc_updater/src/MacBundle.cpp
Normal file
53
mmc_updater/src/MacBundle.cpp
Normal file
@ -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;
|
||||
}
|
||||
|
35
mmc_updater/src/MacBundle.h
Normal file
35
mmc_updater/src/MacBundle.h
Normal file
@ -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;
|
||||
};
|
||||
|
30
mmc_updater/src/Platform.h
Normal file
30
mmc_updater/src/Platform.h
Normal file
@ -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
|
536
mmc_updater/src/ProcessUtils.cpp
Normal file
536
mmc_updater/src/ProcessUtils.cpp
Normal file
@ -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
|
||||
|
97
mmc_updater/src/ProcessUtils.h
Normal file
97
mmc_updater/src/ProcessUtils.h
Normal file
@ -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);
|
||||
};
|
||||
|
63
mmc_updater/src/StandardDirs.cpp
Normal file
63
mmc_updater/src/StandardDirs.cpp
Normal file
@ -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
|
||||
}
|
22
mmc_updater/src/StandardDirs.h
Normal file
22
mmc_updater/src/StandardDirs.h
Normal file
@ -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
|
||||
};
|
||||
|
18
mmc_updater/src/StandardDirs.mm
Normal file
18
mmc_updater/src/StandardDirs.mm
Normal file
@ -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();
|
||||
}
|
||||
|
75
mmc_updater/src/StlSymbolsLeopard.cpp
Normal file
75
mmc_updater/src/StlSymbolsLeopard.cpp
Normal file
@ -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
|
46
mmc_updater/src/StringUtils.h
Normal file
46
mmc_updater/src/StringUtils.h
Normal file
@ -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;
|
||||
}
|
||||
|
25
mmc_updater/src/UpdateDialog.cpp
Normal file
25
mmc_updater/src/UpdateDialog.cpp
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
|
29
mmc_updater/src/UpdateDialog.h
Normal file
29
mmc_updater/src/UpdateDialog.h
Normal file
@ -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;
|
||||
};
|
||||
|
70
mmc_updater/src/UpdateDialogAscii.cpp
Normal file
70
mmc_updater/src/UpdateDialogAscii.cpp
Normal file
@ -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()
|
||||
{
|
||||
}
|
||||
|
32
mmc_updater/src/UpdateDialogAscii.h
Normal file
32
mmc_updater/src/UpdateDialogAscii.h
Normal file
@ -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;
|
||||
};
|
||||
|
32
mmc_updater/src/UpdateDialogCocoa.h
Normal file
32
mmc_updater/src/UpdateDialogCocoa.h
Normal file
@ -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;
|
||||
};
|
||||
|
194
mmc_updater/src/UpdateDialogCocoa.mm
Normal file
194
mmc_updater/src/UpdateDialogCocoa.mm
Normal file
@ -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];
|
||||
}
|
||||
|
||||
|
155
mmc_updater/src/UpdateDialogGtk.cpp
Normal file
155
mmc_updater/src/UpdateDialogGtk.cpp
Normal file
@ -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();
|
||||
}
|
||||
|
||||
|
42
mmc_updater/src/UpdateDialogGtk.h
Normal file
42
mmc_updater/src/UpdateDialogGtk.h
Normal file
@ -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();
|
||||
}
|
||||
|
||||
|
59
mmc_updater/src/UpdateDialogGtkFactory.cpp
Normal file
59
mmc_updater/src/UpdateDialogGtkFactory.cpp
Normal file
@ -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());
|
||||
}
|
||||
|
13
mmc_updater/src/UpdateDialogGtkFactory.h
Normal file
13
mmc_updater/src/UpdateDialogGtkFactory.h
Normal file
@ -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();
|
||||
};
|
||||
|
215
mmc_updater/src/UpdateDialogWin32.cpp
Normal file
215
mmc_updater/src/UpdateDialogWin32.cpp
Normal file
@ -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;
|
||||
}
|
39
mmc_updater/src/UpdateDialogWin32.h
Normal file
39
mmc_updater/src/UpdateDialogWin32.h
Normal file
@ -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;
|
||||
};
|
||||
|
439
mmc_updater/src/UpdateInstaller.cpp
Normal file
439
mmc_updater/src/UpdateInstaller.cpp
Normal file
@ -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;
|
||||
}
|
||||
|
70
mmc_updater/src/UpdateInstaller.h
Normal file
70
mmc_updater/src/UpdateInstaller.h
Normal file
@ -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;
|
||||
};
|
||||
|
42
mmc_updater/src/UpdateMessage.h
Normal file
42
mmc_updater/src/UpdateMessage.h
Normal file
@ -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;
|
||||
}
|
||||
};
|
||||
|
15
mmc_updater/src/UpdateObserver.h
Normal file
15
mmc_updater/src/UpdateObserver.h
Normal file
@ -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;
|
||||
};
|
||||
|
98
mmc_updater/src/UpdateScript.cpp
Normal file
98
mmc_updater/src/UpdateScript.cpp
Normal file
@ -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;
|
||||
}
|
||||
|
86
mmc_updater/src/UpdateScript.h
Normal file
86
mmc_updater/src/UpdateScript.h
Normal file
@ -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;
|
||||
};
|
||||
|
156
mmc_updater/src/UpdaterOptions.cpp
Normal file
156
mmc_updater/src/UpdaterOptions.cpp
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
27
mmc_updater/src/UpdaterOptions.h
Normal file
27
mmc_updater/src/UpdaterOptions.h
Normal file
@ -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);
|
||||
};
|
||||
|
201
mmc_updater/src/main.cpp
Normal file
201
mmc_updater/src/main.cpp
Normal file
@ -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
|
38
mmc_updater/src/resources/Info.plist
Normal file
38
mmc_updater/src/resources/Info.plist
Normal file
@ -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>
|
BIN
mmc_updater/src/resources/icon128.png
Normal file
BIN
mmc_updater/src/resources/icon128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
mmc_updater/src/resources/icon64.png
Normal file
BIN
mmc_updater/src/resources/icon64.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
mmc_updater/src/resources/mac.icns
Normal file
BIN
mmc_updater/src/resources/mac.icns
Normal file
Binary file not shown.
BIN
mmc_updater/src/resources/updater.ico
Normal file
BIN
mmc_updater/src/resources/updater.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
30
mmc_updater/src/resources/updater.rc
Normal file
30
mmc_updater/src/resources/updater.rc
Normal file
@ -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
|
51
mmc_updater/src/tests/CMakeLists.txt
Normal file
51
mmc_updater/src/tests/CMakeLists.txt
Normal file
@ -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)
|
||||
|
50
mmc_updater/src/tests/TestFileUtils.cpp
Normal file
50
mmc_updater/src/tests/TestFileUtils.cpp
Normal file
@ -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);
|
||||
}
|
10
mmc_updater/src/tests/TestFileUtils.h
Normal file
10
mmc_updater/src/tests/TestFileUtils.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
class TestFileUtils
|
||||
{
|
||||
public:
|
||||
void testDirName();
|
||||
void testIsRelative();
|
||||
void testSymlinkFileExists();
|
||||
void testStandardDirs();
|
||||
};
|
48
mmc_updater/src/tests/TestUpdateScript.cpp
Normal file
48
mmc_updater/src/tests/TestUpdateScript.cpp
Normal file
@ -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);
|
||||
}
|
||||
|
9
mmc_updater/src/tests/TestUpdateScript.h
Normal file
9
mmc_updater/src/tests/TestUpdateScript.h
Normal file
@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
class TestUpdateScript
|
||||
{
|
||||
public:
|
||||
void testV2Script();
|
||||
void testPermissions();
|
||||
};
|
||||
|
68
mmc_updater/src/tests/TestUpdaterOptions.cpp
Normal file
68
mmc_updater/src/tests/TestUpdaterOptions.cpp
Normal file
@ -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);
|
||||
}
|
||||
|
8
mmc_updater/src/tests/TestUpdaterOptions.h
Normal file
8
mmc_updater/src/tests/TestUpdaterOptions.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class TestUpdaterOptions
|
||||
{
|
||||
public:
|
||||
void testOldFormatArgs();
|
||||
};
|
||||
|
108
mmc_updater/src/tests/TestUtils.h
Normal file
108
mmc_updater/src/tests/TestUtils.h
Normal file
@ -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);
|
||||
|
||||
|
52
mmc_updater/src/tests/file_list.xml
Normal file
52
mmc_updater/src/tests/file_list.xml
Normal file
@ -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>
|
8
mmc_updater/src/tests/new_app.cpp
Normal file
8
mmc_updater/src/tests/new_app.cpp
Normal file
@ -0,0 +1,8 @@
|
||||
#include <iostream>
|
||||
|
||||
int main(int,char**)
|
||||
{
|
||||
std::cout << "new app starting" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
7
mmc_updater/src/tests/old_app.cpp
Normal file
7
mmc_updater/src/tests/old_app.cpp
Normal file
@ -0,0 +1,7 @@
|
||||
#include <iostream>
|
||||
|
||||
int main(int,char**)
|
||||
{
|
||||
std::cout << "old app starting" << std::endl;
|
||||
return 0;
|
||||
}
|
218
mmc_updater/src/tests/test-update.rb
Executable file
218
mmc_updater/src/tests/test-update.rb
Executable file
@ -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"
|
67
mmc_updater/src/tests/v2_file_list.xml
Normal file
67
mmc_updater/src/tests/v2_file_list.xml
Normal file
@ -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>
|
Reference in New Issue
Block a user