Skip to content

Commit

Permalink
Implement update installer
Browse files Browse the repository at this point in the history
  • Loading branch information
oblivioncth committed Nov 5, 2023
1 parent cbc87ed commit 6345fb3
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 18 deletions.
177 changes: 163 additions & 14 deletions app/src/command/c-update.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,6 @@
#include "task/t-exec.h"
#include "utility.h"

QX_JSON_STRUCT_OUTSIDE(CUpdate::ReleaseAsset,
name,
browser_download_url
)

QX_JSON_STRUCT_OUTSIDE(CUpdate::ReleaseData,
name,
tag_name,
assets
)

//===============================================================================================================
// CUpdateError
//===============================================================================================================
Expand Down Expand Up @@ -84,6 +73,72 @@ QString CUpdate::sanitizeCompiler(QString cmp)
return cmp;
}

QString CUpdate::substituteFolderNames(const QString& path, QStringView binName, QStringView appName)
{
static const QString stdBin = u"bin"_s;
static const QString stdAppName = CLIFP_CANONICAL_APP_FILNAME;

QString subbed = path;

if(subbed.startsWith(stdBin))
subbed = subbed.mid(stdBin.size()).prepend(binName);
if(subbed.endsWith(CLIFP_CANONICAL_APP_FILNAME))
{
subbed.chop(stdAppName.size());
subbed.append(appName);
}

return subbed;
}

Qx::IoOpReport CUpdate::determineNewFiles(QStringList& files, const QDir& sourceRoot)
{
files = {};

QStringList subDirs = {u"bin"_s};
for(const auto& sd : {u"lib"_s, u"plugins"_s})
if(sourceRoot.exists(sd))
subDirs += sd;

for(const auto& sd : qAsConst(subDirs))
{
QStringList subFiles;
Qx::IoOpReport r = Qx::dirContentList(subFiles, sourceRoot.absoluteFilePath(sd), {}, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::Subdirectories);
if(r.isFailure())
return r;

for(auto& p : subFiles)
p.prepend(sd + '/');

files += subFiles;
}

return Qx::IoOpReport(Qx::IO_OP_ENUMERATE, Qx::IO_SUCCESS, sourceRoot);
}

CUpdate::UpdateTransfers CUpdate::determineTransfers(const QStringList& files, const TransferSpecs& specs)
{
UpdateTransfers transfers;

for(const QString& file : files)
{
QString installPath = specs.installRoot.filePath(substituteFolderNames(file, specs.binName, specs.appName));
QString updatePath = specs.updateRoot.filePath(file);
QString backupPath = specs.backupRoot.filePath(file);
transfers.install << FileTransfer{.source = updatePath, .dest = installPath};
transfers.backup << FileTransfer{.source = installPath, .dest = backupPath};
}

return transfers;
}

//Public:
bool CUpdate::clearUpdateCache()
{
QDir updateCache(updateCachePath());
return !updateCache.exists() || updateCache.removeRecursively();
}

//-Instance Functions-------------------------------------------------------------
//Private:
CUpdateError CUpdate::getLatestReleaseData(ReleaseData& data) const
Expand Down Expand Up @@ -143,6 +198,51 @@ QString CUpdate::getTargetAssetName(const QString& tagName) const
);
}

CUpdateError CUpdate::handleTransfers(const UpdateTransfers& transfers) const
{
auto doTransfer = [&](const FileTransfer& ft, bool mkpath, bool move, bool overwrite){
mCore.logEvent(NAME, LOG_EVENT_FILE_TRANSFER.arg(ft.source, ft.dest));

if(mkpath)
{
QDir destDir(QFileInfo(ft.dest).absolutePath());
if(!destDir.mkpath(u"."_s))
return false;
}

if(overwrite && QFile::exists(ft.dest) && !QFile::remove(ft.dest))
return false;

return move ? QFile::rename(ft.source, ft.dest) : QFile::copy(ft.source, ft.dest);
};

// Backup, and note for restore
mCore.logEvent(NAME, LOG_EVENT_BACKUP_FILES);
QList<FileTransfer> restoreTransfers;
QScopeGuard restoreOnFail([&]{
mCore.logEvent(NAME, LOG_EVENT_RESTORE_FILES);
for(const auto& t : restoreTransfers) doTransfer(t, false, true, true);
});

for(const auto& ft : transfers.backup)
{
if(!doTransfer(ft, true, true, true))
return CUpdateError(CUpdateError::TransferFail, ft.dest);
restoreTransfers << FileTransfer{.source = ft.dest, .dest = ft.source};
}

// Install
mCore.logEvent(NAME, LOG_EVENT_INSTALL_FILES);
for(const auto& ft : transfers.install)
{
if(!doTransfer(ft, true, false, false))
return CUpdateError(CUpdateError::TransferFail, ft.dest);
}
restoreOnFail.dismiss();

return CUpdateError();
}

CUpdateError CUpdate::checkAndPrepareUpdate() const
{
// Check for update
Expand Down Expand Up @@ -223,9 +323,58 @@ CUpdateError CUpdate::checkAndPrepareUpdate() const
return CUpdateError();
}

CUpdateError CUpdate::installUpdate(const QString& appPath) const
Qx::Error CUpdate::installUpdate(const QFileInfo& existingAppInfo) const
{
//...
// Wait for previous process to close, lock instance afterwards
static const int totalGrace = 2000;
static const int step = 500;
int currentGrace = 0;
bool haveLock = false;

do
{
mCore.logEvent(NAME, LOG_EVENT_WAITING_ON_OLD_CLOSE.arg(totalGrace - currentGrace));
QThread::msleep(step);
currentGrace += step;
haveLock = !mCore.blockNewInstances().isValid();
}
while(!haveLock && currentGrace < totalGrace);

// TODO: Allow user retry here (i.e. they close the process manually)
if(!haveLock)
return CUpdateError(CUpdateError::OldProcessNotFinished, "Aborting update.");

//-Install update------------------------------------------------------------
mCore.logEvent(NAME, LOG_EVENT_INSTALLING_UPDATE);

// Ensure old executeable exists where expected
if(!existingAppInfo.exists())
return CUpdateError(CUpdateError::InvalidPath, "Missing " + existingAppInfo.absoluteFilePath());

// Note structure
TransferSpecs ts{
.updateRoot = updateDataDir(),
.installRoot = QDir(QDir::cleanPath(existingAppInfo.absoluteFilePath() + "/../..")),
.backupRoot = updateBackupDir(),
.appName = existingAppInfo.fileName(),
.binName = existingAppInfo.absoluteDir().dirName()
};

// Determine transfers
QStringList updateFiles;
if(Qx::IoOpReport rep = determineNewFiles(updateFiles, ts.updateRoot); rep.isFailure())
return rep;

UpdateTransfers updateTransfers = determineTransfers(updateFiles, ts);

// Transfer
if(CUpdateError err = handleTransfers(updateTransfers); err.isValid())
return err;

// Success
mCore.logEvent(NAME, MSG_UPDATE_COMPLETE);
mCore.postMessage(Message{.text = MSG_UPDATE_COMPLETE});
return CUpdateError();
}

//Protected:
Expand All @@ -236,7 +385,7 @@ Qx::Error CUpdate::perform()
{
// Install stage
if(mParser.isSet(CL_OPTION_INSTALL))
return installUpdate(mParser.value(CL_OPTION_INSTALL));
return installUpdate(QFileInfo(mParser.value(CL_OPTION_INSTALL)));

// Prepare stage
CoreError err = mCore.blockNewInstances(); // This command allows multi-instance so we do this manually here
Expand Down
63 changes: 61 additions & 2 deletions app/src/command/c-update.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class QX_ERROR_TYPE(CUpdateError, "CUpdateError", 1218)
ConnectionError,
InvalidUpdateData,
InvalidReleaseVersion,
OldProcessNotFinished,
InvalidPath,
TransferFail
};

//-Class Variables-------------------------------------------------------------
Expand All @@ -27,6 +30,9 @@ class QX_ERROR_TYPE(CUpdateError, "CUpdateError", 1218)
{ConnectionError, u"Failed to query the update server."_s},
{InvalidUpdateData, u"The update server responded with unrecognized data."_s},
{InvalidReleaseVersion, u"The latest release has an invalid version."_s},
{OldProcessNotFinished, u"The old version is still running."_s},
{InvalidPath, u"An update path is invalid."_s},
{TransferFail, u"File transfer operation failed."_s}
};

//-Instance Variables-------------------------------------------------------------
Expand Down Expand Up @@ -59,13 +65,45 @@ class CUpdate : public Command
{
QString name;
QString browser_download_url;

QX_JSON_STRUCT(
name,
browser_download_url
);
};

struct ReleaseData
{
QString name;
QString tag_name;
QList<ReleaseAsset> assets;

QX_JSON_STRUCT(
name,
tag_name,
assets
);
};

struct FileTransfer
{
QString source;
QString dest;
};

struct TransferSpecs
{
QDir updateRoot;
QDir installRoot;
QDir backupRoot;
QString appName;
QString binName;
};

struct UpdateTransfers
{
QList<FileTransfer> install;
QList<FileTransfer> backup;
};

//-Class Variables------------------------------------------------------------------------------------------------------
Expand All @@ -79,17 +117,26 @@ class CUpdate : public Command
// Message
static inline const QString MSG_NO_UPDATES = u"No updates available."_s;
static inline const QString QUES_UPDATE = uR"("%1" is available.\n\nUpdate?)"_s;
static inline const QString MSG_UPDATE_COMPLETE = u"Update installed succesfully."_s;

// Error
static inline const QString WRN_NO_MATCHING_BUILD_P = u"A newer version is available, but without any assets that match current build specifications."_s;
static inline const QString WRN_NO_MATCHING_BUILD_S = u"Update manually at GitHub."_s;

// Log
// Log - Prepare
static inline const QString LOG_EVENT_CHECKING_FOR_NEWER_VERSION = u"Checking if a newer release is available..."_s;
static inline const QString LOG_EVENT_UPDATE_AVAILABLE = u"Update available (%1)."_s;
static inline const QString LOG_EVENT_UPDATE_ACCEPED = u"Queuing update..."_s;
static inline const QString LOG_EVENT_UPDATE_REJECTED = u"Update rejected"_s;

// Log - Install
static inline const QString LOG_EVENT_WAITING_ON_OLD_CLOSE = u"Waiting for bootstrap process to close (%1ms reamining)..."_s;
static inline const QString LOG_EVENT_INSTALLING_UPDATE = u"Installing update..."_s;
static inline const QString LOG_EVENT_FILE_TRANSFER = u"Transfering \"%1\" to \"%2\""_s;
static inline const QString LOG_EVENT_BACKUP_FILES = u"Backing up original files..."_s;
static inline const QString LOG_EVENT_RESTORE_FILES = u"Restoring original files..."_s;
static inline const QString LOG_EVENT_INSTALL_FILES = u"Installing new files..."_s;

// Command line option strings
static inline const QString CL_OPT_INSTALL_L_NAME = u"install"_s;
static inline const QString CL_OPT_INSTALL_DESC = u""_s;
Expand All @@ -114,18 +161,30 @@ class CUpdate : public Command

//-Class Functions------------------------------------------------------------------------------------------------------
private:
// Path
static QString updateCachePath();
static QDir updateDownloadDir();
static QDir updateDataDir();
static QDir updateBackupDir();

// Adjustment
static QString sanitizeCompiler(QString cmp);
static QString substituteFolderNames(const QString& path, QStringView binName, QStringView appName);

// Work
static Qx::IoOpReport determineNewFiles(QStringList& files, const QDir& sourceRoot);
static UpdateTransfers determineTransfers(const QStringList& files, const TransferSpecs& specs);

public:
static bool clearUpdateCache();

//-Instance Functions------------------------------------------------------------------------------------------------------
private:
CUpdateError getLatestReleaseData(ReleaseData& data) const;
QString getTargetAssetName(const QString& tagName) const;
CUpdateError handleTransfers(const UpdateTransfers& transfers) const;
CUpdateError checkAndPrepareUpdate() const;
CUpdateError installUpdate(const QString& appName) const;
Qx::Error installUpdate(const QFileInfo& existingAppInfo) const;

protected:
QList<const QCommandLineOption*> options() override;
Expand Down
7 changes: 7 additions & 0 deletions app/src/kernel/core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

// Project Includes
#include "command/command.h"
#include "command/c-update.h"
#include "task/t-download.h"
#include "task/t-exec.h"
#include "task/t-extract.h"
Expand Down Expand Up @@ -279,6 +280,12 @@ Qx::Error Core::initialize(QStringList& commandLine)
// Log global options
logEvent(NAME, LOG_EVENT_GLOBAL_OPT.arg(globalOptions));

// Clear update cache
if(CUpdate::clearUpdateCache())
logEvent(NAME, LOG_EVENT_CLEARED_UPDATE_CACHE);
else
logError(NAME, CoreError(CoreError::FailedClearingUpdateCache, {}, Qx::Warning));

// Check for valid arguments
if(validArgs)
{
Expand Down
7 changes: 5 additions & 2 deletions app/src/kernel/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class QX_ERROR_TYPE(CoreError, "CoreError", 1200)
TooManyResults,
ConfiguredServerMissing,
DataPackSumMismatch,
DataPackSourceMissing
DataPackSourceMissing,
FailedClearingUpdateCache
};

//-Class Variables-------------------------------------------------------------
Expand All @@ -55,7 +56,8 @@ class QX_ERROR_TYPE(CoreError, "CoreError", 1200)
{TooManyResults, u"More results than can be presented were returned in a search."_s},
{ConfiguredServerMissing, u"The server specified in the Flashpoint config was not found within the Flashpoint services store."_s},
{DataPackSumMismatch, u"The existing Data Pack of the selected title does not contain the data expected. It will be re-downloaded."_s},
{DataPackSourceMissing, u"The expected primary data pack source was missing."_s}
{DataPackSourceMissing, u"The expected primary data pack source was missing."_s},
{FailedClearingUpdateCache, u"Failed to clear the update cache."_s}
};

//-Instance Variables-------------------------------------------------------------
Expand Down Expand Up @@ -150,6 +152,7 @@ class Core : public QObject
// Logging - Messages
static inline const QString LOG_EVENT_INIT = u"Initializing CLIFp..."_s;
static inline const QString LOG_EVENT_GLOBAL_OPT = u"Global Options: %1"_s;
static inline const QString LOG_EVENT_CLEARED_UPDATE_CACHE = u"Cleared update cache (or it wasn't present)."_s;
static inline const QString LOG_EVENT_FURTHER_INSTANCE_BLOCK = u"Attempting to lock instance count..."_s;
static inline const QString LOG_EVENT_G_HELP_SHOWN = u"Displayed general help information"_s;
static inline const QString LOG_EVENT_VER_SHOWN = u"Displayed version information"_s;
Expand Down

0 comments on commit 6345fb3

Please sign in to comment.