Renew the updater branch

Now with some actual consensus on what the updater will do!
This commit is contained in:
Petr Mrázek
2013-12-02 00:55:24 +01:00
parent 613699b362
commit 6aa9bd0f77
118 changed files with 44913 additions and 45 deletions

View 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
View 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";
}

View 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()

View 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
}

View 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
};

View 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
View 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
View 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
View 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)

View 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;
}

View 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;
};

View 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

View 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

View 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);
};

View 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
}

View 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
};

View 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();
}

View 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

View 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;
}

View 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();
}
}

View 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;
};

View 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()
{
}

View 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;
};

View 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;
};

View 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];
}

View 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();
}

View 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();
}

View 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());
}

View 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();
};

View 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;
}

View 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;
};

View 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;
}

View 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;
};

View 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;
}
};

View 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;
};

View 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;
}

View 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;
};

View 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);
}
}

View 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
View 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

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View 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

View 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)

View 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);
}

View File

@ -0,0 +1,10 @@
#pragma once
class TestFileUtils
{
public:
void testDirName();
void testIsRelative();
void testSymlinkFileExists();
void testStandardDirs();
};

View 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);
}

View File

@ -0,0 +1,9 @@
#pragma once
class TestUpdateScript
{
public:
void testV2Script();
void testPermissions();
};

View 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);
}

View File

@ -0,0 +1,8 @@
#pragma once
class TestUpdaterOptions
{
public:
void testOldFormatArgs();
};

View 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);

View 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>

View File

@ -0,0 +1,8 @@
#include <iostream>
int main(int,char**)
{
std::cout << "new app starting" << std::endl;
return 0;
}

View File

@ -0,0 +1,7 @@
#include <iostream>
int main(int,char**)
{
std::cout << "old app starting" << std::endl;
return 0;
}

View 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"

View 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>