feat: 支持 Linux 客户端打包与跨平台运行
This commit is contained in:
@@ -26,14 +26,9 @@ target_include_directories(Common
|
||||
PRIVATE ${OPENSSL_INC}
|
||||
)
|
||||
|
||||
# 链接库、库目录通过INTERFACE传递给所有依赖Common的exe
|
||||
target_link_directories(Common INTERFACE
|
||||
$<$<CONFIG:Debug>:${OPENSSL_LIB_DEBUG}>
|
||||
$<$<NOT:$<CONFIG:Debug>>:${OPENSSL_LIB_RELEASE}>
|
||||
)
|
||||
target_link_libraries(Common
|
||||
PRIVATE Qt5::Core Qt5::Network Qt5::Widgets
|
||||
INTERFACE libssl.lib libcrypto.lib
|
||||
PUBLIC ${SIMCAE_OPENSSL_LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
|
||||
+96
-10
@@ -284,6 +284,26 @@ ConfigHelper& ConfigHelper::instance()
|
||||
return obj;
|
||||
}
|
||||
|
||||
QString ConfigHelper::executableNameForCurrentPlatform(const QString& configuredValue,
|
||||
const QString& fallbackBaseName)
|
||||
{
|
||||
QString name = configuredValue.trimmed();
|
||||
if (name.isEmpty())
|
||||
name = fallbackBaseName.trimmed();
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
const QString fileName = QFileInfo(name).fileName();
|
||||
if (!fileName.endsWith(QStringLiteral(".exe"), Qt::CaseInsensitive)
|
||||
&& QFileInfo(fileName).suffix().isEmpty()) {
|
||||
name += QStringLiteral(".exe");
|
||||
}
|
||||
#else
|
||||
if (name.endsWith(QStringLiteral(".exe"), Qt::CaseInsensitive))
|
||||
name.chop(4);
|
||||
#endif
|
||||
return name;
|
||||
}
|
||||
|
||||
int ConfigHelper::runElevatedWriteCommandIfRequested()
|
||||
{
|
||||
const QStringList arguments = QCoreApplication::arguments();
|
||||
@@ -486,11 +506,30 @@ bool ConfigHelper::syncRegistryFromConfigFileIfChanged()
|
||||
if (previousHash == sourceHash)
|
||||
return true;
|
||||
|
||||
const QJsonObject config = document.object();
|
||||
if (config.isEmpty())
|
||||
{
|
||||
settings.beginGroup(kRegistryMetaGroup);
|
||||
settings.setValue(kRegistrySourceHashKey, sourceHash);
|
||||
settings.setValue(kRegistrySourcePathKey, QDir::toNativeSeparators(QFileInfo(m_configPath).absoluteFilePath()));
|
||||
settings.setValue(kRegistryRuntimeRootKey, QDir::toNativeSeparators(QApplication::applicationDirPath()));
|
||||
settings.setValue(kRegistryImportedAtKey, QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
|
||||
settings.endGroup();
|
||||
settings.sync();
|
||||
if (settings.status() != QSettings::NoError)
|
||||
{
|
||||
m_error = QStringLiteral("Cannot sync empty app_config.json marker to registry.");
|
||||
return false;
|
||||
}
|
||||
m_error.clear();
|
||||
qDebug() << "app_config.json is empty; keeping existing registry configuration.";
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool configChangedAfterPreviousImport = !previousHash.isEmpty();
|
||||
|
||||
settings.beginGroup(kRegistryConfigGroup);
|
||||
settings.remove(QString());
|
||||
const QJsonObject config = document.object();
|
||||
for (auto it = config.constBegin(); it != config.constEnd(); ++it)
|
||||
settings.setValue(it.key(), it.value().toVariant());
|
||||
settings.endGroup();
|
||||
@@ -517,7 +556,8 @@ bool ConfigHelper::syncRegistryFromConfigFileIfChanged()
|
||||
const QString configDir = QFileInfo(m_configPath).absolutePath();
|
||||
const QStringList staleFiles{
|
||||
QDir(configDir).filePath(QStringLiteral("client_identity.dat")),
|
||||
QDir(configDir).filePath(QStringLiteral("version_policy.dat"))
|
||||
QDir(configDir).filePath(QStringLiteral("version_policy.dat")),
|
||||
QDir(configDir).filePath(QStringLiteral("local_state.json"))
|
||||
};
|
||||
for (const QString& staleFile : staleFiles)
|
||||
{
|
||||
@@ -529,8 +569,48 @@ bool ConfigHelper::syncRegistryFromConfigFileIfChanged()
|
||||
return false;
|
||||
}
|
||||
}
|
||||
qDebug() << "Removed stale client identity and policy after app_config.json changed.";
|
||||
qDebug() << "Removed stale client identity, policy and local state after app_config.json changed.";
|
||||
}
|
||||
|
||||
if (!config.isEmpty() && !sanitizeConfigFileAfterImport(settings))
|
||||
{
|
||||
qWarning() << "Cannot sanitize app_config.json after registry import:" << m_error;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigHelper::sanitizeConfigFileAfterImport(QSettings& settings)
|
||||
{
|
||||
const QJsonObject emptyConfig;
|
||||
const QByteArray normalizedEmptyConfig = QJsonDocument(emptyConfig).toJson(QJsonDocument::Compact);
|
||||
const QByteArray emptyFileBytes = QJsonDocument(emptyConfig).toJson(QJsonDocument::Indented);
|
||||
const QString sanitizedHash = QString::fromLatin1(
|
||||
QCryptographicHash::hash(normalizedEmptyConfig, QCryptographicHash::Sha256).toHex());
|
||||
|
||||
QString writeError;
|
||||
if (!ConfigHelper::writeFileWithElevationIfNeeded(m_configPath, emptyFileBytes, &writeError))
|
||||
{
|
||||
m_error = QStringLiteral("Cannot clear app_config.json after registry import: %1").arg(writeError);
|
||||
return false;
|
||||
}
|
||||
|
||||
settings.beginGroup(kRegistryMetaGroup);
|
||||
settings.setValue(kRegistrySourceHashKey, sanitizedHash);
|
||||
settings.setValue(kRegistrySourcePathKey, QDir::toNativeSeparators(QFileInfo(m_configPath).absoluteFilePath()));
|
||||
settings.setValue(kRegistryRuntimeRootKey, QDir::toNativeSeparators(QApplication::applicationDirPath()));
|
||||
settings.setValue(kRegistryImportedAtKey, QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
|
||||
settings.endGroup();
|
||||
settings.sync();
|
||||
|
||||
if (settings.status() != QSettings::NoError)
|
||||
{
|
||||
m_error = QStringLiteral("Cannot update registry source hash after clearing app_config.json.");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_error.clear();
|
||||
qDebug() << "Cleared app_config.json after importing configuration to registry.";
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -631,13 +711,19 @@ bool ConfigHelper::migrateLegacyIniIfNeeded()
|
||||
copyText("Update", "temp_folder", "update_temp");
|
||||
copyText("Update", "device_id");
|
||||
copyText("Runtime", "install_root", ".");
|
||||
copyText("Runtime", "main_executable", "MainApp.exe");
|
||||
copyText("Runtime", "launcher_executable", "Launcher.exe");
|
||||
copyText("Runtime", "updater_executable", "Updater.exe");
|
||||
copyText("Runtime", "bootstrap_executable", "Bootstrap.exe");
|
||||
copyText("Runtime", "health_check_timeout_ms", "15000");
|
||||
config.insert("platform", "windows");
|
||||
config.insert("arch", "x64");
|
||||
copyText("Runtime", "main_executable", ConfigHelper::executableNameForCurrentPlatform(QString(), "MainApp"));
|
||||
copyText("Runtime", "launcher_executable", ConfigHelper::executableNameForCurrentPlatform(QString(), "Launcher"));
|
||||
copyText("Runtime", "updater_executable", ConfigHelper::executableNameForCurrentPlatform(QString(), "Updater"));
|
||||
copyText("Runtime", "bootstrap_executable", ConfigHelper::executableNameForCurrentPlatform(QString(), "Bootstrap"));
|
||||
copyText("Runtime", "health_check_timeout_ms", "15000");
|
||||
#ifdef Q_OS_WIN
|
||||
config.insert("platform", "windows");
|
||||
#elif defined(Q_OS_LINUX)
|
||||
config.insert("platform", "linux");
|
||||
#else
|
||||
config.insert("platform", "unknown");
|
||||
#endif
|
||||
config.insert("arch", "x64");
|
||||
|
||||
QDir().mkpath(QFileInfo(m_configPath).path());
|
||||
QSaveFile output(m_configPath);
|
||||
|
||||
@@ -9,6 +9,8 @@ class ConfigHelper
|
||||
public:
|
||||
static ConfigHelper& instance();
|
||||
static int runElevatedWriteCommandIfRequested();
|
||||
static QString executableNameForCurrentPlatform(const QString& configuredValue,
|
||||
const QString& fallbackBaseName);
|
||||
static bool writeFileWithElevationIfNeeded(const QString& path, const QByteArray& data,
|
||||
QString* errorMessage = nullptr);
|
||||
static bool removeFileWithElevationIfNeeded(const QString& path, QString* errorMessage = nullptr);
|
||||
@@ -26,6 +28,7 @@ private:
|
||||
bool migrateLegacyIniIfNeeded();
|
||||
void enterRegistryGroup(QSettings& settings) const;
|
||||
bool syncRegistryFromConfigFileIfChanged();
|
||||
bool sanitizeConfigFileAfterImport(QSettings& settings);
|
||||
bool readRegistryValue(const QString& key, QString* value) const;
|
||||
bool writeRegistryValue(const QString& key, const QString& value);
|
||||
QString readFileValue(const QString& key) const;
|
||||
|
||||
+74
-19
@@ -1,5 +1,20 @@
|
||||
#include "FileHelper.h"
|
||||
#include <QDebug>
|
||||
#include "FileHelper.h"
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QThread>
|
||||
|
||||
namespace
|
||||
{
|
||||
QString processImageName(const QString& exeName)
|
||||
{
|
||||
QString name = QFileInfo(exeName).fileName().trimmed();
|
||||
#ifndef Q_OS_WIN
|
||||
if (name.endsWith(QStringLiteral(".exe"), Qt::CaseInsensitive))
|
||||
name.chop(4);
|
||||
#endif
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
bool FileHelper::createDir(const QString &path)
|
||||
{
|
||||
@@ -22,21 +37,61 @@ bool FileHelper::copyFileOverwrite(const QString &src, const QString &dst)
|
||||
return QFile::copy(src, dst);
|
||||
}
|
||||
|
||||
bool FileHelper::isProcessRunning(const QString &exeName)
|
||||
{
|
||||
QProcess process;
|
||||
process.start("tasklist");
|
||||
process.waitForFinished();
|
||||
QString output = process.readAllStandardOutput();
|
||||
return output.contains(exeName, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool FileHelper::killProcess(const QString &exeName)
|
||||
{
|
||||
if (!isProcessRunning(exeName))
|
||||
return true;
|
||||
QProcess process;
|
||||
process.start("taskkill /f /im " + exeName);
|
||||
process.waitForFinished(1000);
|
||||
return !isProcessRunning(exeName);
|
||||
bool FileHelper::isProcessRunning(const QString &exeName)
|
||||
{
|
||||
const QString imageName = processImageName(exeName);
|
||||
if (imageName.isEmpty())
|
||||
return false;
|
||||
|
||||
QProcess process;
|
||||
#ifdef Q_OS_WIN
|
||||
process.start(QStringLiteral("tasklist"), QStringList{
|
||||
QStringLiteral("/FI"),
|
||||
QStringLiteral("IMAGENAME eq %1").arg(imageName)
|
||||
});
|
||||
process.waitForFinished();
|
||||
const QString output = QString::fromLocal8Bit(process.readAllStandardOutput());
|
||||
return output.contains(imageName, Qt::CaseInsensitive);
|
||||
#elif defined(Q_OS_UNIX)
|
||||
process.start(QStringLiteral("pgrep"), QStringList{
|
||||
QStringLiteral("-x"),
|
||||
imageName
|
||||
});
|
||||
process.waitForFinished(1000);
|
||||
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool FileHelper::killProcess(const QString &exeName)
|
||||
{
|
||||
if (!isProcessRunning(exeName))
|
||||
return true;
|
||||
const QString imageName = processImageName(exeName);
|
||||
if (imageName.isEmpty())
|
||||
return true;
|
||||
|
||||
QProcess process;
|
||||
#ifdef Q_OS_WIN
|
||||
process.start(QStringLiteral("taskkill"), QStringList{
|
||||
QStringLiteral("/f"),
|
||||
QStringLiteral("/im"),
|
||||
imageName
|
||||
});
|
||||
#elif defined(Q_OS_UNIX)
|
||||
process.start(QStringLiteral("pkill"), QStringList{
|
||||
QStringLiteral("-x"),
|
||||
imageName
|
||||
});
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
process.waitForFinished(3000);
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
if (!isProcessRunning(imageName))
|
||||
return true;
|
||||
QThread::msleep(100);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+11
-11
@@ -28,17 +28,17 @@ bool IntegrityHelper::safeRelativePath(const QString& path) const
|
||||
|
||||
bool IntegrityHelper::runtimeProtectedPath(const QString& path) const
|
||||
{
|
||||
const QString p = QDir::fromNativeSeparators(path).toCaseFolded();
|
||||
QSet<QString> protectedPaths{
|
||||
"bootstrap.exe", "client.ini", "config/app_config.json", "config/local_state.json",
|
||||
"config/client_identity.dat", "config/version_policy.dat"
|
||||
};
|
||||
const QString runtimePrefix = ConfigHelper::instance().runtimeRelativePath().toCaseFolded();
|
||||
if (!runtimePrefix.isEmpty()) {
|
||||
const QStringList runtimeProtected{
|
||||
"bootstrap.exe", "client.ini", "config/app_config.json", "config/local_state.json",
|
||||
"config/client_identity.dat", "config/version_policy.dat"
|
||||
};
|
||||
const QString p = QDir::fromNativeSeparators(path).toCaseFolded();
|
||||
QSet<QString> protectedPaths{
|
||||
"bootstrap", "bootstrap.exe", "client.ini", "config/app_config.json", "config/local_state.json",
|
||||
"config/client_identity.dat", "config/version_policy.dat"
|
||||
};
|
||||
const QString runtimePrefix = ConfigHelper::instance().runtimeRelativePath().toCaseFolded();
|
||||
if (!runtimePrefix.isEmpty()) {
|
||||
const QStringList runtimeProtected{
|
||||
"bootstrap", "bootstrap.exe", "client.ini", "config/app_config.json", "config/local_state.json",
|
||||
"config/client_identity.dat", "config/version_policy.dat"
|
||||
};
|
||||
for (const QString& protectedPath : runtimeProtected)
|
||||
protectedPaths.insert(runtimePrefix + "/" + protectedPath);
|
||||
}
|
||||
|
||||
+28
-12
@@ -77,15 +77,31 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex
|
||||
const QDateTime issued = QDateTime::fromString(payload.value("issued_at").toString(), Qt::ISODate);
|
||||
const QDateTime expires = QDateTime::fromString(payload.value("expires_at").toString(), Qt::ISODate);
|
||||
const QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
const bool identityOk = payload.value("app_id").toString() == expectedAppId
|
||||
&& payload.value("device_id").toString() == expectedDeviceId
|
||||
&& payload.value("version").toString() == expectedVersion;
|
||||
const bool timeOk = issued.isValid() && expires.isValid() && issued <= now.addSecs(5)
|
||||
&& expires >= now && issued.secsTo(expires) <= 65;
|
||||
if (actual.isEmpty() || actual != expected || !identityOk || !timeOk
|
||||
|| payload.value("nonce").toString().isEmpty()) {
|
||||
if (errorMessage) *errorMessage = "ticket signature, identity, time or nonce invalid";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const bool timeOk = issued.isValid() && expires.isValid() && issued <= now.addSecs(5)
|
||||
&& expires >= now && issued.secsTo(expires) <= 65;
|
||||
if (actual.isEmpty() || actual != expected) {
|
||||
if (errorMessage) *errorMessage = "ticket signature invalid; check launch_token";
|
||||
return false;
|
||||
}
|
||||
if (payload.value("app_id").toString() != expectedAppId) {
|
||||
if (errorMessage) *errorMessage = "ticket app_id mismatch";
|
||||
return false;
|
||||
}
|
||||
if (payload.value("device_id").toString() != expectedDeviceId) {
|
||||
if (errorMessage) *errorMessage = "ticket device_id mismatch";
|
||||
return false;
|
||||
}
|
||||
if (payload.value("version").toString() != expectedVersion) {
|
||||
if (errorMessage) *errorMessage = "ticket version mismatch";
|
||||
return false;
|
||||
}
|
||||
if (!timeOk) {
|
||||
if (errorMessage) *errorMessage = "ticket time invalid or expired";
|
||||
return false;
|
||||
}
|
||||
if (payload.value("nonce").toString().isEmpty()) {
|
||||
if (errorMessage) *errorMessage = "ticket nonce missing";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user