GH-1726 better failure detection for updates
Instead of just checking if the new version started, make sure it is able to write its IPC key to a file and then use the key to connect to the process.
This commit is contained in:
parent
e974950d48
commit
69be23c5f6
@ -52,8 +52,8 @@ QDebug operator<<(QDebug dbg, const Operation::Type &t)
|
|||||||
|
|
||||||
QDebug operator<<(QDebug dbg, const Operation &u)
|
QDebug operator<<(QDebug dbg, const Operation &u)
|
||||||
{
|
{
|
||||||
dbg.nospace() << "Operation(type=" << u.type << " file=" << u.file
|
dbg.nospace() << "Operation(type=" << u.type << " file=" << u.source
|
||||||
<< " dest=" << u.dest << " mode=" << u.mode << ")";
|
<< " dest=" << u.destination << " mode=" << u.destinationMode << ")";
|
||||||
return dbg.maybeSpace();
|
return dbg.maybeSpace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,19 +68,22 @@ typedef QList<VersionFileEntry> VersionFileList;
|
|||||||
*/
|
*/
|
||||||
struct MULTIMC_LOGIC_EXPORT Operation
|
struct MULTIMC_LOGIC_EXPORT Operation
|
||||||
{
|
{
|
||||||
static Operation CopyOp(QString fsource, QString fdest, int fmode=0644)
|
static Operation CopyOp(QString from, QString to, int fmode=0644)
|
||||||
{
|
{
|
||||||
return Operation{OP_REPLACE, fsource, fdest, fmode};
|
return Operation{OP_REPLACE, from, to, fmode};
|
||||||
}
|
}
|
||||||
static Operation DeleteOp(QString file)
|
static Operation DeleteOp(QString file)
|
||||||
{
|
{
|
||||||
return Operation{OP_DELETE, file, "", 0644};
|
return Operation{OP_DELETE, QString(), file, 0644};
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: for some types, some of the other fields are irrelevant!
|
// FIXME: for some types, some of the other fields are irrelevant!
|
||||||
bool operator==(const Operation &u2) const
|
bool operator==(const Operation &u2) const
|
||||||
{
|
{
|
||||||
return type == u2.type && file == u2.file && dest == u2.dest && mode == u2.mode;
|
return type == u2.type &&
|
||||||
|
source == u2.source &&
|
||||||
|
destination == u2.destination &&
|
||||||
|
destinationMode == u2.destinationMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
//! Specifies the type of operation that this is.
|
//! Specifies the type of operation that this is.
|
||||||
@ -90,14 +93,14 @@ struct MULTIMC_LOGIC_EXPORT Operation
|
|||||||
OP_DELETE,
|
OP_DELETE,
|
||||||
} type;
|
} type;
|
||||||
|
|
||||||
//! The file to operate on.
|
//! The source file, if any
|
||||||
QString file;
|
QString source;
|
||||||
|
|
||||||
//! The destination file.
|
//! The destination file.
|
||||||
QString dest;
|
QString destination;
|
||||||
|
|
||||||
//! The mode to change the source file to.
|
//! The mode to change the destination file to.
|
||||||
int mode;
|
int destinationMode;
|
||||||
};
|
};
|
||||||
typedef QList<Operation> OperationList;
|
typedef QList<Operation> OperationList;
|
||||||
|
|
||||||
|
@ -85,6 +85,8 @@ SET(MULTIMC_SOURCES
|
|||||||
MultiMC.cpp
|
MultiMC.cpp
|
||||||
BuildConfig.h
|
BuildConfig.h
|
||||||
${PROJECT_BINARY_DIR}/BuildConfig.cpp
|
${PROJECT_BINARY_DIR}/BuildConfig.cpp
|
||||||
|
UpdateController.cpp
|
||||||
|
UpdateController.h
|
||||||
|
|
||||||
# GUI - general utilities
|
# GUI - general utilities
|
||||||
GuiUtil.h
|
GuiUtil.h
|
||||||
|
@ -90,6 +90,7 @@
|
|||||||
#include "dialogs/ExportInstanceDialog.h"
|
#include "dialogs/ExportInstanceDialog.h"
|
||||||
#include <FolderInstanceProvider.h>
|
#include <FolderInstanceProvider.h>
|
||||||
#include <InstanceImportTask.h>
|
#include <InstanceImportTask.h>
|
||||||
|
#include "UpdateController.h"
|
||||||
|
|
||||||
class MainWindow::Ui
|
class MainWindow::Ui
|
||||||
{
|
{
|
||||||
@ -952,7 +953,8 @@ void MainWindow::downloadUpdates(GoUpdate::Status status)
|
|||||||
// If the task succeeds, install the updates.
|
// If the task succeeds, install the updates.
|
||||||
if (updateDlg.execWithTask(&updateTask))
|
if (updateDlg.execWithTask(&updateTask))
|
||||||
{
|
{
|
||||||
MMC->installUpdates(updateTask.updateFilesDir(), updateTask.operations());
|
UpdateController update(this, MMC->root(), updateTask.updateFilesDir(), updateTask.operations());
|
||||||
|
update.installUpdates();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -73,6 +73,8 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static const QLatin1String liveCheckFile("live.check");
|
||||||
|
|
||||||
using namespace Commandline;
|
using namespace Commandline;
|
||||||
|
|
||||||
MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
|
MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
|
||||||
@ -132,6 +134,9 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
|
|||||||
parser.addOption("launch");
|
parser.addOption("launch");
|
||||||
parser.addShortOpt("launch", 'l');
|
parser.addShortOpt("launch", 'l');
|
||||||
parser.addDocumentation("launch", "launch the specified instance (by instance ID)");
|
parser.addDocumentation("launch", "launch the specified instance (by instance ID)");
|
||||||
|
// --alive
|
||||||
|
parser.addSwitch("alive");
|
||||||
|
parser.addDocumentation("alive", "write a small '" + liveCheckFile + "' file after MultiMC starts");
|
||||||
|
|
||||||
// parse the arguments
|
// parse the arguments
|
||||||
try
|
try
|
||||||
@ -165,6 +170,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_instanceIdToLaunch = args["launch"].toString();
|
m_instanceIdToLaunch = args["launch"].toString();
|
||||||
|
m_liveCheck = args["alive"].toBool();
|
||||||
|
|
||||||
QString origcwdPath = QDir::currentPath();
|
QString origcwdPath = QDir::currentPath();
|
||||||
QString binPath = applicationDirPath();
|
QString binPath = applicationDirPath();
|
||||||
@ -242,6 +248,27 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
|
|||||||
qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch;
|
qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do // once
|
||||||
|
{
|
||||||
|
if(m_liveCheck)
|
||||||
|
{
|
||||||
|
QFile check(liveCheckFile);
|
||||||
|
if(!check.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||||
|
{
|
||||||
|
qWarning() << "Could not open" << liveCheckFile << "for writing!";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto payload = appID.toString().toUtf8();
|
||||||
|
if(check.write(payload) != payload.size())
|
||||||
|
{
|
||||||
|
qWarning() << "Could not write into" << liveCheckFile;
|
||||||
|
check.remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
check.close();
|
||||||
|
}
|
||||||
|
} while(false);
|
||||||
|
|
||||||
// load settings
|
// load settings
|
||||||
initGlobalSettings();
|
initGlobalSettings();
|
||||||
|
|
||||||
@ -697,319 +724,6 @@ std::shared_ptr<JavaInstallList> MultiMC::javalist()
|
|||||||
return m_javalist;
|
return m_javalist;
|
||||||
}
|
}
|
||||||
|
|
||||||
// from <sys/stat.h>
|
|
||||||
#ifndef S_IRUSR
|
|
||||||
#define __S_IREAD 0400 /* Read by owner. */
|
|
||||||
#define __S_IWRITE 0200 /* Write by owner. */
|
|
||||||
#define __S_IEXEC 0100 /* Execute by owner. */
|
|
||||||
#define S_IRUSR __S_IREAD /* Read by owner. */
|
|
||||||
#define S_IWUSR __S_IWRITE /* Write by owner. */
|
|
||||||
#define S_IXUSR __S_IEXEC /* Execute by owner. */
|
|
||||||
|
|
||||||
#define S_IRGRP (S_IRUSR >> 3) /* Read by group. */
|
|
||||||
#define S_IWGRP (S_IWUSR >> 3) /* Write by group. */
|
|
||||||
#define S_IXGRP (S_IXUSR >> 3) /* Execute by group. */
|
|
||||||
|
|
||||||
#define S_IROTH (S_IRGRP >> 3) /* Read by others. */
|
|
||||||
#define S_IWOTH (S_IWGRP >> 3) /* Write by others. */
|
|
||||||
#define S_IXOTH (S_IXGRP >> 3) /* Execute by others. */
|
|
||||||
#endif
|
|
||||||
static QFile::Permissions unixModeToPermissions(const int mode)
|
|
||||||
{
|
|
||||||
QFile::Permissions perms;
|
|
||||||
|
|
||||||
if (mode & S_IRUSR)
|
|
||||||
{
|
|
||||||
perms |= QFile::ReadUser;
|
|
||||||
}
|
|
||||||
if (mode & S_IWUSR)
|
|
||||||
{
|
|
||||||
perms |= QFile::WriteUser;
|
|
||||||
}
|
|
||||||
if (mode & S_IXUSR)
|
|
||||||
{
|
|
||||||
perms |= QFile::ExeUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode & S_IRGRP)
|
|
||||||
{
|
|
||||||
perms |= QFile::ReadGroup;
|
|
||||||
}
|
|
||||||
if (mode & S_IWGRP)
|
|
||||||
{
|
|
||||||
perms |= QFile::WriteGroup;
|
|
||||||
}
|
|
||||||
if (mode & S_IXGRP)
|
|
||||||
{
|
|
||||||
perms |= QFile::ExeGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode & S_IROTH)
|
|
||||||
{
|
|
||||||
perms |= QFile::ReadOther;
|
|
||||||
}
|
|
||||||
if (mode & S_IWOTH)
|
|
||||||
{
|
|
||||||
perms |= QFile::WriteOther;
|
|
||||||
}
|
|
||||||
if (mode & S_IXOTH)
|
|
||||||
{
|
|
||||||
perms |= QFile::ExeOther;
|
|
||||||
}
|
|
||||||
return perms;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MultiMC::installUpdates(const QString updateFilesDir, GoUpdate::OperationList operations)
|
|
||||||
{
|
|
||||||
qint64 pid = -1;
|
|
||||||
QStringList args;
|
|
||||||
bool started = false;
|
|
||||||
|
|
||||||
qDebug() << "Installing updates.";
|
|
||||||
#ifdef Q_OS_WIN
|
|
||||||
QString finishCmd = applicationFilePath();
|
|
||||||
#elif defined Q_OS_LINUX
|
|
||||||
QString finishCmd = FS::PathCombine(root(), "MultiMC");
|
|
||||||
#elif defined Q_OS_MAC
|
|
||||||
QString finishCmd = applicationFilePath();
|
|
||||||
#else
|
|
||||||
#error Unsupported operating system.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
QString backupPath = FS::PathCombine(root(), "update", "backup");
|
|
||||||
QDir origin(root());
|
|
||||||
|
|
||||||
// clean up the backup folder. it should be empty before we start
|
|
||||||
if(!FS::deletePath(backupPath))
|
|
||||||
{
|
|
||||||
qWarning() << "couldn't remove previous backup folder" << backupPath;
|
|
||||||
}
|
|
||||||
// and it should exist.
|
|
||||||
if(!FS::ensureFolderPathExists(backupPath))
|
|
||||||
{
|
|
||||||
qWarning() << "couldn't create folder" << backupPath;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct BackupEntry
|
|
||||||
{
|
|
||||||
QString orig;
|
|
||||||
QString backup;
|
|
||||||
};
|
|
||||||
enum Failure
|
|
||||||
{
|
|
||||||
Replace,
|
|
||||||
Delete,
|
|
||||||
Start,
|
|
||||||
Nothing
|
|
||||||
} failedOperationType = Nothing;
|
|
||||||
QString failedFile;
|
|
||||||
|
|
||||||
QList <BackupEntry> backups;
|
|
||||||
QList <BackupEntry> trashcan;
|
|
||||||
|
|
||||||
bool useXPHack = false;
|
|
||||||
QString exePath;
|
|
||||||
QString exeOrigin;
|
|
||||||
QString exeBackup;
|
|
||||||
|
|
||||||
// perform the update operations
|
|
||||||
for(auto op: operations)
|
|
||||||
{
|
|
||||||
switch(op.type)
|
|
||||||
{
|
|
||||||
// replace = move original out to backup, if it exists, move the new file in its place
|
|
||||||
case GoUpdate::Operation::OP_REPLACE:
|
|
||||||
{
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
// hack for people renaming the .exe because ... reasons :)
|
|
||||||
if(op.dest == "MultiMC.exe")
|
|
||||||
{
|
|
||||||
op.dest = QFileInfo(applicationFilePath()).fileName();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
QFileInfo replaced (FS::PathCombine(root(), op.dest));
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
if(QSysInfo::windowsVersion() < QSysInfo::WV_VISTA)
|
|
||||||
{
|
|
||||||
if(replaced.fileName() == "MultiMC.exe")
|
|
||||||
{
|
|
||||||
QDir rootDir(root());
|
|
||||||
exeOrigin = rootDir.relativeFilePath(op.file);
|
|
||||||
exePath = rootDir.relativeFilePath(op.dest);
|
|
||||||
exeBackup = rootDir.relativeFilePath(FS::PathCombine(backupPath, replaced.fileName()));
|
|
||||||
useXPHack = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if(replaced.exists())
|
|
||||||
{
|
|
||||||
QString backupName = op.dest;
|
|
||||||
backupName.replace('/', '_');
|
|
||||||
QString backupFilePath = FS::PathCombine(backupPath, backupName);
|
|
||||||
if(!QFile::rename(replaced.absoluteFilePath(), backupFilePath))
|
|
||||||
{
|
|
||||||
qWarning() << "Couldn't move:" << replaced.absoluteFilePath() << "to" << backupFilePath;
|
|
||||||
failedOperationType = Replace;
|
|
||||||
failedFile = op.dest;
|
|
||||||
goto FAILED;
|
|
||||||
}
|
|
||||||
BackupEntry be;
|
|
||||||
be.orig = replaced.absoluteFilePath();
|
|
||||||
be.backup = backupFilePath;
|
|
||||||
backups.append(be);
|
|
||||||
}
|
|
||||||
// make sure the folder we are putting this into exists
|
|
||||||
if(!FS::ensureFilePathExists(replaced.absoluteFilePath()))
|
|
||||||
{
|
|
||||||
qWarning() << "REPLACE: Couldn't create folder:" << replaced.absoluteFilePath();
|
|
||||||
failedOperationType = Replace;
|
|
||||||
failedFile = op.dest;
|
|
||||||
goto FAILED;
|
|
||||||
}
|
|
||||||
// now move the new file in
|
|
||||||
if(!QFile::rename(op.file, replaced.absoluteFilePath()))
|
|
||||||
{
|
|
||||||
qWarning() << "REPLACE: Couldn't move:" << op.file << "to" << replaced.absoluteFilePath();
|
|
||||||
failedOperationType = Replace;
|
|
||||||
failedFile = op.dest;
|
|
||||||
goto FAILED;
|
|
||||||
}
|
|
||||||
QFile::setPermissions(replaced.absoluteFilePath(), unixModeToPermissions(op.mode));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
// delete = move original to backup
|
|
||||||
case GoUpdate::Operation::OP_DELETE:
|
|
||||||
{
|
|
||||||
QString origFilePath = FS::PathCombine(root(), op.file);
|
|
||||||
if(QFile::exists(origFilePath))
|
|
||||||
{
|
|
||||||
QString backupName = op.file;
|
|
||||||
backupName.replace('/', '_');
|
|
||||||
QString trashFilePath = FS::PathCombine(backupPath, backupName);
|
|
||||||
|
|
||||||
if(!QFile::rename(origFilePath, trashFilePath))
|
|
||||||
{
|
|
||||||
qWarning() << "DELETE: Couldn't move:" << op.file << "to" << trashFilePath;
|
|
||||||
failedFile = op.file;
|
|
||||||
failedOperationType = Delete;
|
|
||||||
goto FAILED;
|
|
||||||
}
|
|
||||||
BackupEntry be;
|
|
||||||
be.orig = origFilePath;
|
|
||||||
be.backup = trashFilePath;
|
|
||||||
trashcan.append(be);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to start the new binary
|
|
||||||
args = qApp->arguments();
|
|
||||||
args.removeFirst();
|
|
||||||
|
|
||||||
// on old Windows, do insane things... no error checking here, this is just to have something.
|
|
||||||
if(useXPHack)
|
|
||||||
{
|
|
||||||
QString script;
|
|
||||||
auto nativePath = QDir::toNativeSeparators(exePath);
|
|
||||||
auto nativeOriginPath = QDir::toNativeSeparators(exeOrigin);
|
|
||||||
auto nativeBackupPath = QDir::toNativeSeparators(exeBackup);
|
|
||||||
|
|
||||||
// so we write this vbscript thing...
|
|
||||||
QTextStream out(&script);
|
|
||||||
out << "WScript.Sleep 1000\n";
|
|
||||||
out << "Set fso=CreateObject(\"Scripting.FileSystemObject\")\n";
|
|
||||||
out << "Set shell=CreateObject(\"WScript.Shell\")\n";
|
|
||||||
out << "fso.MoveFile \"" << nativePath << "\", \"" << nativeBackupPath << "\"\n";
|
|
||||||
out << "fso.MoveFile \"" << nativeOriginPath << "\", \"" << nativePath << "\"\n";
|
|
||||||
out << "shell.Run \"" << nativePath << "\"\n";
|
|
||||||
|
|
||||||
QString scriptPath = FS::PathCombine(root(), "update", "update.vbs");
|
|
||||||
|
|
||||||
// we save it
|
|
||||||
QFile scriptFile(scriptPath);
|
|
||||||
scriptFile.open(QIODevice::WriteOnly);
|
|
||||||
scriptFile.write(script.toLocal8Bit().replace("\n", "\r\n"));
|
|
||||||
scriptFile.close();
|
|
||||||
|
|
||||||
// we run it
|
|
||||||
started = QProcess::startDetached("wscript", {scriptPath}, root());
|
|
||||||
|
|
||||||
// and we quit. conscious thought.
|
|
||||||
qApp->quit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
started = QProcess::startDetached(finishCmd, args, QDir::currentPath(), &pid);
|
|
||||||
// failed to start... ?
|
|
||||||
if(!started || pid == -1)
|
|
||||||
{
|
|
||||||
qWarning() << "Couldn't start new process properly!";
|
|
||||||
failedOperationType = Start;
|
|
||||||
goto FAILED;
|
|
||||||
}
|
|
||||||
origin.rmdir(updateFilesDir);
|
|
||||||
qApp->quit();
|
|
||||||
return;
|
|
||||||
|
|
||||||
FAILED:
|
|
||||||
qWarning() << "Update failed!";
|
|
||||||
bool revertOK = true;
|
|
||||||
// if the above failed, roll back changes
|
|
||||||
for(auto backup:backups)
|
|
||||||
{
|
|
||||||
qWarning() << "restoring" << backup.orig << "from" << backup.backup;
|
|
||||||
if(!QFile::remove(backup.orig))
|
|
||||||
{
|
|
||||||
revertOK = false;
|
|
||||||
qWarning() << "removing new" << backup.orig << "failed!";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!QFile::rename(backup.backup, backup.orig))
|
|
||||||
{
|
|
||||||
revertOK = false;
|
|
||||||
qWarning() << "restoring" << backup.orig << "failed!";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(auto backup:trashcan)
|
|
||||||
{
|
|
||||||
qWarning() << "restoring" << backup.orig << "from" << backup.backup;
|
|
||||||
if(!QFile::rename(backup.backup, backup.orig))
|
|
||||||
{
|
|
||||||
revertOK = false;
|
|
||||||
qWarning() << "restoring" << backup.orig << "failed!";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QString msg;
|
|
||||||
if(!revertOK)
|
|
||||||
{
|
|
||||||
msg = tr("The update failed and then the update revert failed too.\n"
|
|
||||||
"You will have to repair MultiMC manually.\n"
|
|
||||||
"Please let us know why and how this happened.").arg(failedFile);
|
|
||||||
}
|
|
||||||
else switch (failedOperationType)
|
|
||||||
{
|
|
||||||
case Replace:
|
|
||||||
msg = tr("Couldn't replace file %1. Changes were reverted.\n"
|
|
||||||
"See the MultiMC log file for details.").arg(failedFile);
|
|
||||||
break;
|
|
||||||
case Delete:
|
|
||||||
msg = tr("Couldn't remove file %1. Changes were reverted.\n"
|
|
||||||
"See the MultiMC log file for details.").arg(failedFile);
|
|
||||||
break;
|
|
||||||
case Start:
|
|
||||||
msg = tr("The new version didn't start and the update was rolled back.");
|
|
||||||
break;
|
|
||||||
case Nothing:
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QMessageBox::critical(nullptr, tr("Update failed!"), msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<ITheme *> MultiMC::getValidApplicationThemes()
|
std::vector<ITheme *> MultiMC::getValidApplicationThemes()
|
||||||
{
|
{
|
||||||
std::vector<ITheme *> ret;
|
std::vector<ITheme *> ret;
|
||||||
|
@ -132,9 +132,6 @@ public:
|
|||||||
return m_rootPath;
|
return m_rootPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// install updates now.
|
|
||||||
void installUpdates(const QString updateFilesDir, GoUpdate::OperationList operations);
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Opens a json file using either a system default editor, or, if not empty, the editor
|
* Opens a json file using either a system default editor, or, if not empty, the editor
|
||||||
* specified in the settings
|
* specified in the settings
|
||||||
@ -223,5 +220,6 @@ private:
|
|||||||
LocalPeer * m_peerInstance = nullptr;
|
LocalPeer * m_peerInstance = nullptr;
|
||||||
public:
|
public:
|
||||||
QString m_instanceIdToLaunch;
|
QString m_instanceIdToLaunch;
|
||||||
|
bool m_liveCheck = false;
|
||||||
std::unique_ptr<QFile> logFile;
|
std::unique_ptr<QFile> logFile;
|
||||||
};
|
};
|
||||||
|
431
application/UpdateController.cpp
Normal file
431
application/UpdateController.cpp
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
#include <QFile>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <FileSystem.h>
|
||||||
|
#include <updater/GoUpdate.h>
|
||||||
|
#include "UpdateController.h"
|
||||||
|
#include <QApplication>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
#include <LocalPeer.h>
|
||||||
|
|
||||||
|
// from <sys/stat.h>
|
||||||
|
#ifndef S_IRUSR
|
||||||
|
#define __S_IREAD 0400 /* Read by owner. */
|
||||||
|
#define __S_IWRITE 0200 /* Write by owner. */
|
||||||
|
#define __S_IEXEC 0100 /* Execute by owner. */
|
||||||
|
#define S_IRUSR __S_IREAD /* Read by owner. */
|
||||||
|
#define S_IWUSR __S_IWRITE /* Write by owner. */
|
||||||
|
#define S_IXUSR __S_IEXEC /* Execute by owner. */
|
||||||
|
|
||||||
|
#define S_IRGRP (S_IRUSR >> 3) /* Read by group. */
|
||||||
|
#define S_IWGRP (S_IWUSR >> 3) /* Write by group. */
|
||||||
|
#define S_IXGRP (S_IXUSR >> 3) /* Execute by group. */
|
||||||
|
|
||||||
|
#define S_IROTH (S_IRGRP >> 3) /* Read by others. */
|
||||||
|
#define S_IWOTH (S_IWGRP >> 3) /* Write by others. */
|
||||||
|
#define S_IXOTH (S_IXGRP >> 3) /* Execute by others. */
|
||||||
|
#endif
|
||||||
|
static QFile::Permissions unixModeToPermissions(const int mode)
|
||||||
|
{
|
||||||
|
QFile::Permissions perms;
|
||||||
|
|
||||||
|
if (mode & S_IRUSR)
|
||||||
|
{
|
||||||
|
perms |= QFile::ReadUser;
|
||||||
|
}
|
||||||
|
if (mode & S_IWUSR)
|
||||||
|
{
|
||||||
|
perms |= QFile::WriteUser;
|
||||||
|
}
|
||||||
|
if (mode & S_IXUSR)
|
||||||
|
{
|
||||||
|
perms |= QFile::ExeUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode & S_IRGRP)
|
||||||
|
{
|
||||||
|
perms |= QFile::ReadGroup;
|
||||||
|
}
|
||||||
|
if (mode & S_IWGRP)
|
||||||
|
{
|
||||||
|
perms |= QFile::WriteGroup;
|
||||||
|
}
|
||||||
|
if (mode & S_IXGRP)
|
||||||
|
{
|
||||||
|
perms |= QFile::ExeGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode & S_IROTH)
|
||||||
|
{
|
||||||
|
perms |= QFile::ReadOther;
|
||||||
|
}
|
||||||
|
if (mode & S_IWOTH)
|
||||||
|
{
|
||||||
|
perms |= QFile::WriteOther;
|
||||||
|
}
|
||||||
|
if (mode & S_IXOTH)
|
||||||
|
{
|
||||||
|
perms |= QFile::ExeOther;
|
||||||
|
}
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const QLatin1String liveCheckFile("live.check");
|
||||||
|
|
||||||
|
UpdateController::UpdateController(QWidget * parent, const QString& root, const QString updateFilesDir, GoUpdate::OperationList operations)
|
||||||
|
{
|
||||||
|
m_parent = parent;
|
||||||
|
m_root = root;
|
||||||
|
m_updateFilesDir = updateFilesDir;
|
||||||
|
m_operations = operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void UpdateController::installUpdates()
|
||||||
|
{
|
||||||
|
qint64 pid = -1;
|
||||||
|
QStringList args;
|
||||||
|
bool started = false;
|
||||||
|
|
||||||
|
qDebug() << "Installing updates.";
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
QString finishCmd = QApplication::applicationFilePath();
|
||||||
|
#elif defined Q_OS_LINUX
|
||||||
|
QString finishCmd = FS::PathCombine(m_root, "MultiMC");
|
||||||
|
#elif defined Q_OS_MAC
|
||||||
|
QString finishCmd = QApplication::applicationFilePath();
|
||||||
|
#else
|
||||||
|
#error Unsupported operating system.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
QString backupPath = FS::PathCombine(m_root, "update", "backup");
|
||||||
|
QDir origin(m_root);
|
||||||
|
|
||||||
|
// clean up the backup folder. it should be empty before we start
|
||||||
|
if(!FS::deletePath(backupPath))
|
||||||
|
{
|
||||||
|
qWarning() << "couldn't remove previous backup folder" << backupPath;
|
||||||
|
}
|
||||||
|
// and it should exist.
|
||||||
|
if(!FS::ensureFolderPathExists(backupPath))
|
||||||
|
{
|
||||||
|
qWarning() << "couldn't create folder" << backupPath;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool useXPHack = false;
|
||||||
|
QString exePath;
|
||||||
|
QString exeOrigin;
|
||||||
|
QString exeBackup;
|
||||||
|
|
||||||
|
// perform the update operations
|
||||||
|
for(auto op: m_operations)
|
||||||
|
{
|
||||||
|
switch(op.type)
|
||||||
|
{
|
||||||
|
// replace = move original out to backup, if it exists, move the new file in its place
|
||||||
|
case GoUpdate::Operation::OP_REPLACE:
|
||||||
|
{
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// hack for people renaming the .exe because ... reasons :)
|
||||||
|
if(op.destination == "MultiMC.exe")
|
||||||
|
{
|
||||||
|
op.destination = QFileInfo(QApplication::applicationFilePath()).fileName();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
QFileInfo destination (FS::PathCombine(m_root, op.destination));
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
if(QSysInfo::windowsVersion() < QSysInfo::WV_VISTA)
|
||||||
|
{
|
||||||
|
if(destination.fileName() == "MultiMC.exe")
|
||||||
|
{
|
||||||
|
QDir rootDir(m_root);
|
||||||
|
exeOrigin = rootDir.relativeFilePath(op.source);
|
||||||
|
exePath = rootDir.relativeFilePath(op.destination);
|
||||||
|
exeBackup = rootDir.relativeFilePath(FS::PathCombine(backupPath, destination.fileName()));
|
||||||
|
useXPHack = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if(destination.exists())
|
||||||
|
{
|
||||||
|
QString backupName = op.destination;
|
||||||
|
backupName.replace('/', '_');
|
||||||
|
QString backupFilePath = FS::PathCombine(backupPath, backupName);
|
||||||
|
if(!QFile::rename(destination.absoluteFilePath(), backupFilePath))
|
||||||
|
{
|
||||||
|
qWarning() << "Couldn't move:" << destination.absoluteFilePath() << "to" << backupFilePath;
|
||||||
|
m_failedOperationType = Replace;
|
||||||
|
m_failedFile = op.destination;
|
||||||
|
fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BackupEntry be;
|
||||||
|
be.original = destination.absoluteFilePath();
|
||||||
|
be.backup = backupFilePath;
|
||||||
|
be.update = op.source;
|
||||||
|
m_replace_backups.append(be);
|
||||||
|
}
|
||||||
|
// make sure the folder we are putting this into exists
|
||||||
|
if(!FS::ensureFilePathExists(destination.absoluteFilePath()))
|
||||||
|
{
|
||||||
|
qWarning() << "REPLACE: Couldn't create folder:" << destination.absoluteFilePath();
|
||||||
|
m_failedOperationType = Replace;
|
||||||
|
m_failedFile = op.destination;
|
||||||
|
fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// now move the new file in
|
||||||
|
if(!QFile::rename(op.source, destination.absoluteFilePath()))
|
||||||
|
{
|
||||||
|
qWarning() << "REPLACE: Couldn't move:" << op.source << "to" << destination.absoluteFilePath();
|
||||||
|
m_failedOperationType = Replace;
|
||||||
|
m_failedFile = op.destination;
|
||||||
|
fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QFile::setPermissions(destination.absoluteFilePath(), unixModeToPermissions(op.destinationMode));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// delete = move original to backup
|
||||||
|
case GoUpdate::Operation::OP_DELETE:
|
||||||
|
{
|
||||||
|
QString destFilePath = FS::PathCombine(m_root, op.destination);
|
||||||
|
if(QFile::exists(destFilePath))
|
||||||
|
{
|
||||||
|
QString backupName = op.destination;
|
||||||
|
backupName.replace('/', '_');
|
||||||
|
QString trashFilePath = FS::PathCombine(backupPath, backupName);
|
||||||
|
|
||||||
|
if(!QFile::rename(destFilePath, trashFilePath))
|
||||||
|
{
|
||||||
|
qWarning() << "DELETE: Couldn't move:" << op.destination << "to" << trashFilePath;
|
||||||
|
m_failedFile = op.destination;
|
||||||
|
m_failedOperationType = Delete;
|
||||||
|
fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BackupEntry be;
|
||||||
|
be.original = destFilePath;
|
||||||
|
be.backup = trashFilePath;
|
||||||
|
m_delete_backups.append(be);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to start the new binary
|
||||||
|
args = qApp->arguments();
|
||||||
|
args.removeFirst();
|
||||||
|
|
||||||
|
// on old Windows, do insane things... no error checking here, this is just to have something.
|
||||||
|
if(useXPHack)
|
||||||
|
{
|
||||||
|
QString script;
|
||||||
|
auto nativePath = QDir::toNativeSeparators(exePath);
|
||||||
|
auto nativeOriginPath = QDir::toNativeSeparators(exeOrigin);
|
||||||
|
auto nativeBackupPath = QDir::toNativeSeparators(exeBackup);
|
||||||
|
|
||||||
|
// so we write this vbscript thing...
|
||||||
|
QTextStream out(&script);
|
||||||
|
out << "WScript.Sleep 1000\n";
|
||||||
|
out << "Set fso=CreateObject(\"Scripting.FileSystemObject\")\n";
|
||||||
|
out << "Set shell=CreateObject(\"WScript.Shell\")\n";
|
||||||
|
out << "fso.MoveFile \"" << nativePath << "\", \"" << nativeBackupPath << "\"\n";
|
||||||
|
out << "fso.MoveFile \"" << nativeOriginPath << "\", \"" << nativePath << "\"\n";
|
||||||
|
out << "shell.Run \"" << nativePath << "\"\n";
|
||||||
|
|
||||||
|
QString scriptPath = FS::PathCombine(m_root, "update", "update.vbs");
|
||||||
|
|
||||||
|
// we save it
|
||||||
|
QFile scriptFile(scriptPath);
|
||||||
|
scriptFile.open(QIODevice::WriteOnly);
|
||||||
|
scriptFile.write(script.toLocal8Bit().replace("\n", "\r\n"));
|
||||||
|
scriptFile.close();
|
||||||
|
|
||||||
|
// we run it
|
||||||
|
started = QProcess::startDetached("wscript", {scriptPath}, m_root);
|
||||||
|
|
||||||
|
// and we quit. conscious thought.
|
||||||
|
qApp->quit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool doLiveCheck = true;
|
||||||
|
bool startFailed = false;
|
||||||
|
|
||||||
|
// remove live check file, if any
|
||||||
|
if(QFile::exists(liveCheckFile))
|
||||||
|
{
|
||||||
|
if(!QFile::remove(liveCheckFile))
|
||||||
|
{
|
||||||
|
qWarning() << "Couldn't remove the" << liveCheckFile << "file! We will proceed without :(";
|
||||||
|
doLiveCheck = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(doLiveCheck)
|
||||||
|
{
|
||||||
|
if(!args.contains("--alive"))
|
||||||
|
{
|
||||||
|
args.append("--alive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start the updated application
|
||||||
|
started = QProcess::startDetached(finishCmd, args, QDir::currentPath(), &pid);
|
||||||
|
// much dumber check - just find out if the call
|
||||||
|
if(!started || pid == -1)
|
||||||
|
{
|
||||||
|
qWarning() << "Couldn't start new process properly!";
|
||||||
|
startFailed = true;
|
||||||
|
}
|
||||||
|
if(!startFailed && doLiveCheck)
|
||||||
|
{
|
||||||
|
int attempts = 0;
|
||||||
|
while(attempts < 10)
|
||||||
|
{
|
||||||
|
attempts++;
|
||||||
|
QString key;
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||||
|
if(!QFile::exists(liveCheckFile))
|
||||||
|
{
|
||||||
|
qWarning() << "Couldn't find the" << liveCheckFile << "file!";
|
||||||
|
startFailed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
key = QString::fromUtf8(FS::read(liveCheckFile));
|
||||||
|
auto id = ApplicationId::fromRawString(key);
|
||||||
|
LocalPeer peer(nullptr, id);
|
||||||
|
if(peer.isClient())
|
||||||
|
{
|
||||||
|
startFailed = false;
|
||||||
|
qDebug() << "Found process started with key " << key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
startFailed = true;
|
||||||
|
qDebug() << "Process started with key " << key << "apparently died or is not reponding...";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
qWarning() << "Couldn't read the" << liveCheckFile << "file!";
|
||||||
|
startFailed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(startFailed)
|
||||||
|
{
|
||||||
|
m_failedOperationType = Start;
|
||||||
|
fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
origin.rmdir(m_updateFilesDir);
|
||||||
|
qApp->quit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateController::fail()
|
||||||
|
{
|
||||||
|
qWarning() << "Update failed!";
|
||||||
|
|
||||||
|
QString msg;
|
||||||
|
bool doRollback = false;
|
||||||
|
QString failTitle = QObject::tr("Update failed!");
|
||||||
|
QString rollFailTitle = QObject::tr("Rollback failed!");
|
||||||
|
switch (m_failedOperationType)
|
||||||
|
{
|
||||||
|
case Replace:
|
||||||
|
{
|
||||||
|
msg = QObject::tr("Couldn't replace file %1. Changes will be reverted.\n"
|
||||||
|
"See the MultiMC log file for details.").arg(m_failedFile);
|
||||||
|
doRollback = true;
|
||||||
|
QMessageBox::critical(m_parent, failTitle, msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Delete:
|
||||||
|
{
|
||||||
|
msg = QObject::tr("Couldn't remove file %1. Changes will be reverted.\n"
|
||||||
|
"See the MultiMC log file for details.").arg(m_failedFile);
|
||||||
|
doRollback = true;
|
||||||
|
QMessageBox::critical(m_parent, failTitle, msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Start:
|
||||||
|
{
|
||||||
|
msg = QObject::tr("The new version didn't start or is too old and doesn't respond to startup checks.\n"
|
||||||
|
"\n"
|
||||||
|
"Roll back to previous version?");
|
||||||
|
auto result = QMessageBox::critical(
|
||||||
|
m_parent,
|
||||||
|
failTitle,
|
||||||
|
msg,
|
||||||
|
QMessageBox::Yes | QMessageBox::No,
|
||||||
|
QMessageBox::Yes
|
||||||
|
);
|
||||||
|
doRollback = (result == QMessageBox::Yes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Nothing:
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(doRollback)
|
||||||
|
{
|
||||||
|
auto rollbackOK = rollback();
|
||||||
|
if(!rollbackOK)
|
||||||
|
{
|
||||||
|
msg = QObject::tr("The rollback failed too.\n"
|
||||||
|
"You will have to repair MultiMC manually.\n"
|
||||||
|
"Please let us know why and how this happened.").arg(m_failedFile);
|
||||||
|
QMessageBox::critical(m_parent, rollFailTitle, msg);
|
||||||
|
qApp->quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qApp->quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UpdateController::rollback()
|
||||||
|
{
|
||||||
|
bool revertOK = true;
|
||||||
|
// if the above failed, roll back changes
|
||||||
|
for(auto backup:m_replace_backups)
|
||||||
|
{
|
||||||
|
qWarning() << "restoring" << backup.original << "from" << backup.backup;
|
||||||
|
if(!QFile::rename(backup.original, backup.update))
|
||||||
|
{
|
||||||
|
revertOK = false;
|
||||||
|
qWarning() << "moving new" << backup.original << "back to" << backup.update << "failed!";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!QFile::rename(backup.backup, backup.original))
|
||||||
|
{
|
||||||
|
revertOK = false;
|
||||||
|
qWarning() << "restoring" << backup.original << "failed!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(auto backup:m_delete_backups)
|
||||||
|
{
|
||||||
|
qWarning() << "restoring" << backup.original << "from" << backup.backup;
|
||||||
|
if(!QFile::rename(backup.backup, backup.original))
|
||||||
|
{
|
||||||
|
revertOK = false;
|
||||||
|
qWarning() << "restoring" << backup.original << "failed!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return revertOK;
|
||||||
|
}
|
44
application/UpdateController.h
Normal file
44
application/UpdateController.h
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QList>
|
||||||
|
#include <updater/GoUpdate.h>
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
class UpdateController
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
UpdateController(QWidget * parent, const QString &root, const QString updateFilesDir, GoUpdate::OperationList operations);
|
||||||
|
void installUpdates();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void fail();
|
||||||
|
bool rollback();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_root;
|
||||||
|
QString m_updateFilesDir;
|
||||||
|
GoUpdate::OperationList m_operations;
|
||||||
|
QWidget * m_parent;
|
||||||
|
|
||||||
|
struct BackupEntry
|
||||||
|
{
|
||||||
|
// path where we got the new file from
|
||||||
|
QString update;
|
||||||
|
// path of what is being actually updated
|
||||||
|
QString original;
|
||||||
|
// path where the backup of the updated file was placed
|
||||||
|
QString backup;
|
||||||
|
};
|
||||||
|
QList <BackupEntry> m_replace_backups;
|
||||||
|
QList <BackupEntry> m_delete_backups;
|
||||||
|
enum Failure
|
||||||
|
{
|
||||||
|
Replace,
|
||||||
|
Delete,
|
||||||
|
Start,
|
||||||
|
Nothing
|
||||||
|
} m_failedOperationType = Nothing;
|
||||||
|
QString m_failedFile;
|
||||||
|
};
|
@ -4,8 +4,30 @@
|
|||||||
#include <InstanceList.h>
|
#include <InstanceList.h>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
|
// #define BREAK_INFINITE_LOOP
|
||||||
|
// #define BREAK_EXCEPTION
|
||||||
|
// #define BREAK_RETURN
|
||||||
|
|
||||||
|
#ifdef BREAK_INFINITE_LOOP
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
#ifdef BREAK_INFINITE_LOOP
|
||||||
|
while(true)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef BREAK_EXCEPTION
|
||||||
|
throw 42;
|
||||||
|
#endif
|
||||||
|
#ifdef BREAK_RETURN
|
||||||
|
return 42;
|
||||||
|
#endif
|
||||||
|
|
||||||
// initialize Qt
|
// initialize Qt
|
||||||
MultiMC app(argc, argv);
|
MultiMC app(argc, argv);
|
||||||
|
|
||||||
|
@ -54,8 +54,11 @@ public: /* methods */
|
|||||||
static ApplicationId fromTraditionalApp();
|
static ApplicationId fromTraditionalApp();
|
||||||
// ID based on a path with all the application data (no two instances with the same data path should run)
|
// ID based on a path with all the application data (no two instances with the same data path should run)
|
||||||
static ApplicationId fromPathAndVersion(const QString & dataPath, const QString & version);
|
static ApplicationId fromPathAndVersion(const QString & dataPath, const QString & version);
|
||||||
// fully custom ID
|
// custom ID
|
||||||
static ApplicationId fromCustomId(const QString & id);
|
static ApplicationId fromCustomId(const QString & id);
|
||||||
|
// custom ID, based on a raw string previously acquired from 'toString'
|
||||||
|
static ApplicationId fromRawString(const QString & id);
|
||||||
|
|
||||||
|
|
||||||
QString toString()
|
QString toString()
|
||||||
{
|
{
|
||||||
|
@ -108,6 +108,11 @@ ApplicationId ApplicationId::fromCustomId(const QString& id)
|
|||||||
return ApplicationId(QLatin1String("qtsingleapp-") + id);
|
return ApplicationId(QLatin1String("qtsingleapp-") + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplicationId ApplicationId::fromRawString(const QString& id)
|
||||||
|
{
|
||||||
|
return ApplicationId(id);
|
||||||
|
}
|
||||||
|
|
||||||
LocalPeer::LocalPeer(QObject * parent, const ApplicationId &appId)
|
LocalPeer::LocalPeer(QObject * parent, const ApplicationId &appId)
|
||||||
: QObject(parent), id(appId)
|
: QObject(parent), id(appId)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user