diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index 0c7e8b6..398daea 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -59,6 +59,7 @@ SOURCES += \ src/infrastructure/plc_communication_service.cpp \ src/infrastructure/plc_discovery_service.cpp \ src/infrastructure/application_settings_loader.cpp \ + src/infrastructure/runtime_project_bundle.cpp \ src/infrastructure/json_project_storage.cpp \ src/ui/hmi_editor_widget.cpp \ src/ui/logic_editor_widget.cpp @@ -113,6 +114,7 @@ HEADERS += \ src/infrastructure/plc_communication_service.h \ src/infrastructure/plc_discovery_service.h \ src/infrastructure/application_settings_loader.h \ + src/infrastructure/runtime_project_bundle.h \ src/infrastructure/json_project_storage.h \ src/ui/hmi_editor_widget.h \ src/ui/logic_editor_widget.h diff --git a/app/src/infrastructure/runtime_project_bundle.cpp b/app/src/infrastructure/runtime_project_bundle.cpp new file mode 100644 index 0000000..dc20ad3 --- /dev/null +++ b/app/src/infrastructure/runtime_project_bundle.cpp @@ -0,0 +1,234 @@ +/** + * @file runtime_project_bundle.cpp + * @brief 实现用户运行程序工程数据封装服务 + * @version 1.0.0 + * @author suyu + * @date 2026-08-26 + */ + +#include "runtime_project_bundle.h" + +#include +#include +#include +#include + +#include + +namespace { + +constexpr int kMagicSize = 16; +constexpr int kFooterSize = kMagicSize + 4 + 8 + 32; +constexpr quint32 kFormatVersion = 1; +constexpr qint64 kCopyBufferSize = 64 * 1024; +const QByteArray kMagic = QByteArrayLiteral("QTPROX_RUNTIME_B"); + +static_assert(sizeof("QTPROX_RUNTIME_B") - 1 == kMagicSize, + "runtime bundle magic must have a fixed length"); + +void appendUint32LittleEndian(QByteArray *bytes, quint32 value) +{ + for (int shift = 0; shift < 32; shift += 8) + { + bytes->append(static_cast((value >> shift) & 0xff)); + } +} + +void appendUint64LittleEndian(QByteArray *bytes, quint64 value) +{ + for (int shift = 0; shift < 64; shift += 8) + { + bytes->append(static_cast((value >> shift) & 0xff)); + } +} + +quint32 readUint32LittleEndian(const char *bytes) +{ + quint32 value = 0; + for (int shift = 0; shift < 32; shift += 8) + { + value |= static_cast( + static_cast(bytes[shift / 8])) + << shift; + } + return value; +} + +quint64 readUint64LittleEndian(const char *bytes) +{ + quint64 value = 0; + for (int shift = 0; shift < 64; shift += 8) + { + value |= static_cast( + static_cast(bytes[shift / 8])) + << shift; + } + return value; +} + +QByteArray makeFooter(quint64 project_size, const QByteArray &hash) +{ + QByteArray footer; + footer.reserve(kFooterSize); + footer.append(kMagic); + appendUint32LittleEndian(&footer, kFormatVersion); + appendUint64LittleEndian(&footer, project_size); + footer.append(hash); + return footer; +} + +RuntimeProjectBundleLoadResult invalidBundle(const QString &message) +{ + return {RuntimeProjectBundleStatus::Invalid, {}, message}; +} + +} // namespace + +RuntimeProjectBundleWriteResult RuntimeProjectBundleService::write( + const QString &template_executable, + const QString &project_file, + const QString &destination_executable) +{ + if (!QFileInfo::exists(template_executable)) + { + return {false, + QObject::tr("未找到用户运行程序模板:%1") + .arg(template_executable)}; + } + if (!QFileInfo::exists(project_file)) + { + return {false, + QObject::tr("未找到待封装的工程文件:%1").arg(project_file)}; + } + if (QFileInfo::exists(destination_executable)) + { + return {false, + QObject::tr("导出 exe 已经存在:%1") + .arg(destination_executable)}; + } + + const QString temporary_executable = destination_executable + + QStringLiteral(".runtime-bundle-tmp"); + QFile::remove(temporary_executable); + if (!QFile::copy(template_executable, temporary_executable)) + { + return {false, + QObject::tr("复制用户运行程序模板失败:%1") + .arg(template_executable)}; + } + + QFile output(temporary_executable); + QFile project(project_file); + if (!output.open(QIODevice::ReadWrite | QIODevice::Append) + || !project.open(QIODevice::ReadOnly)) + { + output.close(); + project.close(); + QFile::remove(temporary_executable); + return {false, QObject::tr("无法打开用户运行程序封装文件")}; + } + + QCryptographicHash hash(QCryptographicHash::Sha256); + quint64 project_size = 0; + bool succeeded = true; + while (!project.atEnd()) + { + const QByteArray chunk = project.read(kCopyBufferSize); + if (chunk.isEmpty() && project.error() != QFileDevice::NoError) + { + succeeded = false; + break; + } + if (chunk.isEmpty()) + { + break; + } + if (output.write(chunk) != chunk.size()) + { + succeeded = false; + break; + } + hash.addData(chunk); + project_size += static_cast(chunk.size()); + } + + if (succeeded) + { + const QByteArray footer = makeFooter(project_size, hash.result()); + succeeded = output.write(footer) == footer.size(); + } + output.flush(); + output.close(); + project.close(); + if (!succeeded || !QFile::rename(temporary_executable, destination_executable)) + { + QFile::remove(temporary_executable); + return {false, QObject::tr("写入用户运行程序工程数据失败")}; + } + return {true, {}}; +} + +RuntimeProjectBundleLoadResult RuntimeProjectBundleService::load( + const QString &executable_path) +{ + QFile executable(executable_path); + if (!executable.open(QIODevice::ReadOnly)) + { + return invalidBundle( + QObject::tr("无法读取用户运行程序:%1").arg(executable_path)); + } + + const qint64 executable_size = executable.size(); + if (executable_size < kFooterSize) + { + return {RuntimeProjectBundleStatus::NotFound, {}, {}}; + } + if (!executable.seek(executable_size - kFooterSize)) + { + return invalidBundle(QObject::tr("无法定位用户运行程序工程数据")); + } + const QByteArray footer = executable.read(kFooterSize); + if (footer.size() != kFooterSize) + { + return invalidBundle(QObject::tr("用户运行程序工程数据不完整")); + } + if (footer.left(kMagicSize) != kMagic) + { + return {RuntimeProjectBundleStatus::NotFound, {}, {}}; + } + + const quint32 version = readUint32LittleEndian(footer.constData() + kMagicSize); + const quint64 project_size = readUint64LittleEndian( + footer.constData() + kMagicSize + 4); + if (version != kFormatVersion) + { + return invalidBundle(QObject::tr("不支持的用户运行程序工程数据版本")); + } + if (project_size > static_cast( + executable_size - static_cast(kFooterSize)) + || project_size > static_cast(std::numeric_limits::max())) + { + return invalidBundle(QObject::tr("用户运行程序工程数据长度无效")); + } + + const qint64 project_offset = executable_size - kFooterSize + - static_cast(project_size); + if (!executable.seek(project_offset)) + { + return invalidBundle(QObject::tr("无法读取用户运行程序工程数据")); + } + const QByteArray project_data = executable.read( + static_cast(project_size)); + if (project_data.size() != static_cast(project_size)) + { + return invalidBundle(QObject::tr("用户运行程序工程数据不完整")); + } + + const QByteArray expected_hash = footer.right(32); + if (QCryptographicHash::hash(project_data, QCryptographicHash::Sha256) + != expected_hash) + { + return invalidBundle(QObject::tr("用户运行程序工程数据校验失败")); + } + return {RuntimeProjectBundleStatus::Loaded, project_data, {}}; +} diff --git a/app/src/infrastructure/runtime_project_bundle.h b/app/src/infrastructure/runtime_project_bundle.h new file mode 100644 index 0000000..85e6e3e --- /dev/null +++ b/app/src/infrastructure/runtime_project_bundle.h @@ -0,0 +1,50 @@ +/** + * @file runtime_project_bundle.h + * @brief 定义用户运行程序工程数据封装服务 + * @version 1.0.0 + * @author suyu + * @date 2026-08-26 + */ + +#pragma once + +#include +#include + +/** 用户运行程序工程数据的读取状态 */ +enum class RuntimeProjectBundleStatus +{ + NotFound, + Loaded, + Invalid +}; + +/** 从用户运行程序中读取的工程数据 */ +struct RuntimeProjectBundleLoadResult +{ + RuntimeProjectBundleStatus status = RuntimeProjectBundleStatus::NotFound; + QByteArray project_data; + QString message; +}; + +/** 创建用户运行程序的结果 */ +struct RuntimeProjectBundleWriteResult +{ + bool succeeded = false; + QString message; +}; + +/** 将工程数据追加到已编译运行程序并负责校验读取 */ +class RuntimeProjectBundleService final +{ +public: + /** 将工程文件追加到运行程序模板末尾 */ + static RuntimeProjectBundleWriteResult write( + const QString &template_executable, + const QString &project_file, + const QString &destination_executable); + + /** 从当前 exe 末尾读取工程数据并校验完整性 */ + static RuntimeProjectBundleLoadResult load( + const QString &executable_path); +}; diff --git a/app/src/main.cpp b/app/src/main.cpp index 3004868..568162d 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -17,6 +17,7 @@ #include "infrastructure/plc_communication_service.h" #include "infrastructure/plc_discovery_service.h" #include "infrastructure/plc_register_repository.h" +#include "infrastructure/runtime_project_bundle.h" #include "domain/active_register_repository.h" #include "domain/virtual_register_repository.h" #include "services/hmi_editor_service.h" @@ -53,6 +54,8 @@ int main(int argc, char *argv[]) runtime_project_path = arguments.at(project_option + 1); } const QFileInfo executable_info(application.applicationFilePath()); + const RuntimeProjectBundleLoadResult bundled_project_result = + RuntimeProjectBundleService::load(executable_info.absoluteFilePath()); const QString bundled_project = executable_info.absolutePath() + QStringLiteral("/runtime-project.json"); if (runtime_project_path.isEmpty() @@ -63,8 +66,21 @@ int main(int argc, char *argv[]) } const bool embedded_runtime_project = QFile::exists( QStringLiteral(":/runtime-project.json")); + if (bundled_project_result.status == RuntimeProjectBundleStatus::Invalid + && runtime_project_path.isEmpty() + && !embedded_runtime_project) + { + QMessageBox::critical( + nullptr, + QObject::tr("运行程序启动失败"), + bundled_project_result.message); + return 2; + } + const bool bundled_runtime_project = bundled_project_result.status + == RuntimeProjectBundleStatus::Loaded; const bool user_runtime_mode = !runtime_project_path.isEmpty() - || embedded_runtime_project; + || embedded_runtime_project + || bundled_runtime_project; const ApplicationSettingsLoadResult application_settings = ApplicationSettingsLoader::load( @@ -102,6 +118,25 @@ int main(int argc, char *argv[]) result = project_service.load(temporary_path.toUtf8().toStdString()); QFile::remove(temporary_path); } + else if (bundled_runtime_project && runtime_project_path.isEmpty()) + { + const QString temporary_path = QDir::tempPath() + + QStringLiteral("/qtproxinje-runtime-bundle-project.json"); + QFile temporary_file(temporary_path); + const QByteArray &bytes = bundled_project_result.project_data; + if (bytes.isEmpty() || !temporary_file.open(QIODevice::WriteOnly) + || temporary_file.write(bytes) != bytes.size()) + { + QMessageBox::critical( + nullptr, + QObject::tr("运行程序启动失败"), + QObject::tr("无法读取封装的工程资源")); + return 2; + } + temporary_file.close(); + result = project_service.load(temporary_path.toUtf8().toStdString()); + QFile::remove(temporary_path); + } else { result = project_service.load( diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index cf7986a..fbebaf4 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -25,6 +25,7 @@ #include "ui_main_window.h" #include "domain/project_limits.h" +#include "infrastructure/runtime_project_bundle.h" #include #include @@ -51,11 +52,9 @@ #include #include #include -#include #include #include #include -#include #include @@ -469,7 +468,6 @@ void MainWindow::initializeUserRuntime() ui_->hmiToolBar->setVisible(false); ui_->logicToolBar->setVisible(false); ui_->outputDock->setVisible(false); - ui_->dataMonitorDock->setVisible(false); ui_->projectDock->setVisible(false); ui_->propertiesDock->setVisible(false); QTimer::singleShot( @@ -537,16 +535,6 @@ void MainWindow::requestUserRuntimeMode(ApplicationMode requested_mode) MainWindow::~MainWindow() { - if (runtime_export_process_ != nullptr - && runtime_export_process_->state() != QProcess::NotRunning) - { - runtime_export_process_->kill(); - runtime_export_process_->waitForFinished(1000); - } - if (!runtime_export_temp_project_.isEmpty()) - { - QFile::remove(runtime_export_temp_project_); - } qApp->removeEventFilter(this); register_monitor_service_.setAddressesChangedCallback({}); runtime_mode_service_.setPlcStatusChangedCallback({}); @@ -569,16 +557,6 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) void MainWindow::closeEvent(QCloseEvent *event) { - if (runtime_export_process_ != nullptr - && runtime_export_process_->state() != QProcess::NotRunning) - { - QMessageBox::information( - this, - tr("正在导出用户运行程序"), - tr("导出尚未完成,请等待打包结束后再关闭编程器")); - event->ignore(); - return; - } if (!confirmSaveBeforeDestructiveAction()) { event->ignore(); @@ -879,6 +857,11 @@ void MainWindow::configureActions() { showLogicNodeProperties(selected_logic_node_id_); } + else + { + // 数据监控页不对应 HMI 或梯形图对象 + showControlProperties({}); + } if (index == 0 || index == 1) { refreshProjectUi(); @@ -1057,12 +1040,10 @@ void MainWindow::configureRuntimeMonitor() void MainWindow::configureDataMonitor() { - data_monitor_dock_ = ui_->dataMonitorDock; data_monitor_widget_ = new FreeMonitorWidget( - register_monitor_service_, data_monitor_dock_); + register_monitor_service_, ui_->dataMonitorTab); data_monitor_widget_->setObjectName(QStringLiteral("dataMonitorWidget")); - ui_->dataMonitorDockLayout->addWidget(data_monitor_widget_); - ui_->viewMenu->addAction(data_monitor_dock_->toggleViewAction()); + ui_->dataMonitorLayout->addWidget(data_monitor_widget_); connect(data_monitor_widget_, &FreeMonitorWidget::monitorAddressesChanged, this, @@ -1120,10 +1101,6 @@ void MainWindow::refreshDataMonitorUi() || online_connected); data_monitor_widget_->refreshValues( mode, runtime_mode_service_.plcConnectionState()); - if (data_monitor_dock_ != nullptr) - { - data_monitor_dock_->setVisible(mode == ApplicationMode::Editing); - } } void MainWindow::configureProjectTree() @@ -1925,15 +1902,6 @@ void MainWindow::loadProject() void MainWindow::exportRuntimeProgram() { - if (runtime_export_process_ != nullptr) - { - showProjectResult( - tr("导出用户运行程序"), - tr("已有一个导出任务正在执行,请等待完成"), - false); - return; - } - std::string validation_error; if (!project_service_.project().validateForRunning( project_service_.projectLimits(), &validation_error)) @@ -2006,8 +1974,6 @@ void MainWindow::exportRuntimeProgram() const QString temp_project = QDir::tempPath() + QStringLiteral("/qtproxinje-runtime-") + QString::number(QCoreApplication::applicationPid()) - + QStringLiteral("-") - + QString::number(QDateTime::currentMSecsSinceEpoch()) + QStringLiteral(".json"); const ProjectOperationResult save_result = project_service_.exportAs( temp_project.toUtf8().toStdString()); @@ -2017,181 +1983,115 @@ void MainWindow::exportRuntimeProgram() return; } - QString script_path; - QDir probe(QCoreApplication::applicationDirPath()); - for (int depth = 0; depth < 5 && script_path.isEmpty(); ++depth) - { - const QString candidate = probe.filePath(QStringLiteral("scripts/package_qt_app.ps1")); - if (QFileInfo::exists(candidate)) - { - script_path = QDir::toNativeSeparators(candidate); - break; - } - if (!probe.cdUp()) - { - break; - } - } - if (script_path.isEmpty()) - { - QFile::remove(temp_project); - showProjectResult( - tr("导出用户运行程序"), - tr("未找到打包脚本,请确认程序安装目录完整"), - false); - return; - } - - runtime_export_temp_project_ = temp_project; - runtime_export_output_name_ = output_name; - runtime_export_destination_directory_ = destination_directory; - runtime_export_package_directory_ = QDir(probe.filePath(QStringLiteral("build/package"))) - .filePath(output_name); - runtime_export_process_output_.clear(); - runtime_export_completion_handled_ = false; - - runtime_export_progress_ = std::make_unique( + QProgressDialog progress( tr("正在准备导出用户运行程序…"), QString(), 0, 100, this); - runtime_export_progress_->setWindowTitle(tr("导出用户运行程序")); - runtime_export_progress_->setWindowModality(Qt::WindowModal); - runtime_export_progress_->setAutoClose(false); - runtime_export_progress_->setAutoReset(false); - runtime_export_progress_->setMinimumDuration(0); - runtime_export_progress_->setValue(5); - runtime_export_progress_->show(); - - runtime_export_process_ = std::make_unique(this); - runtime_export_process_->setProgram(QStringLiteral("pwsh")); - runtime_export_process_->setArguments({ - QStringLiteral("-NoLogo"), QStringLiteral("-NoProfile"), - QStringLiteral("-File"), script_path, - QStringLiteral("-OutputName"), output_name, - QStringLiteral("-ProjectFile"), temp_project}); - connect( - runtime_export_process_.get(), - &QProcess::readyReadStandardOutput, - this, - &MainWindow::handleRuntimeExportOutput); - connect( - runtime_export_process_.get(), - &QProcess::readyReadStandardError, - this, - &MainWindow::handleRuntimeExportOutput); - connect( - runtime_export_process_.get(), - qOverload(&QProcess::finished), - this, - &MainWindow::handleRuntimeExportFinished); - connect( - runtime_export_process_.get(), - &QProcess::errorOccurred, - this, - [this](QProcess::ProcessError error) - { - if (error == QProcess::FailedToStart) - { - handleRuntimeExportFinished(-1, QProcess::CrashExit); - } - }); - + progress.setWindowTitle(tr("导出用户运行程序")); + progress.setWindowModality(Qt::WindowModal); + progress.setAutoClose(false); + progress.setAutoReset(false); + progress.setMinimumDuration(0); + progress.setValue(5); + progress.show(); + QApplication::processEvents(); ui_->exportRuntimeAction->setEnabled(false); - statusBar()->showMessage(tr("正在构建用户运行程序,请稍候…"), 0); - runtime_export_process_->start(); -} -void MainWindow::handleRuntimeExportOutput() -{ - if (runtime_export_process_ == nullptr) + const QString destination_executable = QDir(destination_directory).filePath( + output_name + QStringLiteral(".exe")); + const QString template_executable = QCoreApplication::applicationFilePath(); + const auto failExport = [this, &progress, &temp_project, &destination_directory]( + const QString &message) { - return; - } - runtime_export_process_output_ += QString::fromUtf8( - runtime_export_process_->readAllStandardOutput()); - runtime_export_process_output_ += QString::fromUtf8( - runtime_export_process_->readAllStandardError()); + QFile::remove(temp_project); + QDir(destination_directory).removeRecursively(); + progress.setValue(0); + progress.close(); + ui_->exportRuntimeAction->setEnabled( + runtime_mode_service_.policy().allowsProjectEditing); + showProjectResult(tr("导出用户运行程序"), message, false); + }; - if (runtime_export_progress_ == nullptr) + if (!QDir().mkpath(destination_directory)) { + failExport(tr("无法创建导出目录:%1").arg(destination_directory)); return; } - if (runtime_export_process_output_.contains(QStringLiteral("RUNTIME_EXPORT_STAGE: qmake"))) - { - runtime_export_progress_->setValue(20); - runtime_export_progress_->setLabelText(tr("正在生成 Qt 构建文件…")); - } - if (runtime_export_process_output_.contains(QStringLiteral("RUNTIME_EXPORT_STAGE: compile"))) - { - runtime_export_progress_->setValue(35); - runtime_export_progress_->setLabelText(tr("正在编译用户运行程序,这一步可能需要较长时间…")); - } - if (runtime_export_process_output_.contains(QStringLiteral("RUNTIME_EXPORT_STAGE: package"))) - { - runtime_export_progress_->setValue(85); - runtime_export_progress_->setLabelText(tr("正在复制 Qt 运行库和平台插件…")); - } - if (runtime_export_process_output_.contains(QStringLiteral("RUNTIME_EXPORT_STAGE: verify"))) - { - runtime_export_progress_->setValue(95); - runtime_export_progress_->setLabelText(tr("正在校验导出结果…")); - } -} -void MainWindow::handleRuntimeExportFinished( - int exit_code, QProcess::ExitStatus exit_status) -{ - if (runtime_export_completion_handled_) + progress.setLabelText(tr("正在封装工程数据…")); + progress.setValue(35); + QApplication::processEvents(); + const RuntimeProjectBundleWriteResult bundle_result = + RuntimeProjectBundleService::write( + template_executable, + temp_project, + destination_executable); + if (!bundle_result.succeeded) { + failExport(bundle_result.message); return; } - runtime_export_completion_handled_ = true; - handleRuntimeExportOutput(); - const QString temp_project = runtime_export_temp_project_; - const QString output_name = runtime_export_output_name_; - const QString destination_directory = runtime_export_destination_directory_; - const QString package_directory = runtime_export_package_directory_; - const QString process_output = runtime_export_process_output_.trimmed(); - const bool process_succeeded = exit_status == QProcess::NormalExit && exit_code == 0; - - bool succeeded = process_succeeded; - QString result_message; - if (!succeeded) - { - result_message = process_output.isEmpty() - ? tr("打包进程未能正常完成") - : tr("打包失败:%1").arg(process_output); + progress.setLabelText(tr("正在复制 Qt 运行库和平台插件…")); + progress.setValue(65); + QApplication::processEvents(); + const QString application_directory = QFileInfo(template_executable).absolutePath(); + const QStringList runtime_files = { + QStringLiteral("Qt5Core.dll"), + QStringLiteral("Qt5Gui.dll"), + QStringLiteral("Qt5Network.dll"), + QStringLiteral("Qt5SerialBus.dll"), + QStringLiteral("Qt5SerialPort.dll"), + QStringLiteral("Qt5Widgets.dll"), + QStringLiteral("libgcc_s_seh-1.dll"), + QStringLiteral("libstdc++-6.dll"), + QStringLiteral("libwinpthread-1.dll")}; + for (const QString &runtime_file : runtime_files) + { + const QString source = QDir(application_directory).filePath(runtime_file); + const QString destination = QDir(destination_directory).filePath(runtime_file); + if (!QFileInfo::exists(source) || !QFile::copy(source, destination)) + { + failExport(tr("复制运行库失败:%1").arg(source)); + return; + } } - else - { - runtime_export_progress_->setValue(98); - runtime_export_progress_->setLabelText(tr("正在整理导出目录…")); - QString copy_error; - succeeded = copyDirectoryContents(package_directory, destination_directory, ©_error); - result_message = succeeded - ? tr("已导出:%1").arg(QDir(destination_directory).filePath(output_name + QStringLiteral(".exe"))) - : copy_error; + QString copy_error; + if (!copyDirectoryContents( + QDir(application_directory).filePath(QStringLiteral("platforms")), + QDir(destination_directory).filePath(QStringLiteral("platforms")), + ©_error) + || !copyDirectoryContents( + QDir(application_directory).filePath(QStringLiteral("styles")), + QDir(destination_directory).filePath(QStringLiteral("styles")), + ©_error)) + { + failExport(copy_error); + return; } + const RuntimeProjectBundleLoadResult verification = + RuntimeProjectBundleService::load(destination_executable); + if (verification.status != RuntimeProjectBundleStatus::Loaded) + { + failExport(verification.message.isEmpty() + ? tr("导出结果校验失败") + : verification.message); + return; + } QFile::remove(temp_project); - if (runtime_export_progress_ != nullptr) - { - runtime_export_progress_->setValue(succeeded ? 100 : 0); - runtime_export_progress_->close(); - runtime_export_progress_.reset(); - } - runtime_export_process_.reset(); - runtime_export_temp_project_.clear(); - runtime_export_output_name_.clear(); - runtime_export_destination_directory_.clear(); - runtime_export_package_directory_.clear(); - runtime_export_process_output_.clear(); + progress.setLabelText(tr("导出完成")); + progress.setValue(100); + QApplication::processEvents(); + progress.close(); ui_->exportRuntimeAction->setEnabled( runtime_mode_service_.policy().allowsProjectEditing); - showProjectResult(tr("导出用户运行程序"), result_message, succeeded); + showProjectResult( + tr("导出用户运行程序"), + tr("已导出:%1").arg(destination_executable), + true); } void MainWindow::showProjectResult( diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index 22871a9..ea3486f 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -15,7 +15,6 @@ #include "services/application_settings.h" #include -#include #include #include @@ -27,9 +26,7 @@ class QActionGroup; class QCloseEvent; class QEvent; class QLabel; -class QDockWidget; class QTimer; -class QProgressDialog; namespace Ui { class MainWindow; @@ -134,7 +131,7 @@ private: void configureLogicEditor(); /** 创建并连接运行监控面板 */ void configureRuntimeMonitor(); - /** 创建并连接编辑态数据监控面板 */ + /** 创建并连接数据监控编辑页 */ void configureDataMonitor(); /** 创建并连接工程树 */ void configureProjectTree(); @@ -216,10 +213,6 @@ private: void loadProject(); /** 将当前工程构建为用户可直接运行的专用 exe */ void exportRuntimeProgram(); - /** 更新用户运行程序导出的阶段进度 */ - void handleRuntimeExportOutput(); - /** 完成用户运行程序导出并复制最终目录 */ - void handleRuntimeExportFinished(int exit_code, QProcess::ExitStatus exit_status); /** 在状态栏和输出面板显示工程操作结果 */ void showProjectResult(const QString &action, const QString &message, bool succeeded); /** 向输出面板追加一条消息 */ @@ -302,8 +295,7 @@ private: LogicEditorWidget *logic_editor_widget_ = nullptr; /** 唯一的运行监控控件 */ RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; - /** 主窗口中的数据监控面板 */ - QDockWidget *data_monitor_dock_ = nullptr; + /** 主窗口中的数据监控编辑页 */ FreeMonitorWidget *data_monitor_widget_ = nullptr; QTimer *data_monitor_refresh_timer_ = nullptr; /** 显示当前运行模式的状态标签 */ @@ -324,16 +316,6 @@ private: std::string current_logic_id_; /** 是否已经安排了待处理的 PLC 状态刷新 */ bool plc_status_update_pending_ = false; - /** 用户运行程序导出进程及其临时状态 */ - std::unique_ptr runtime_export_process_; - std::unique_ptr runtime_export_progress_; - QString runtime_export_temp_project_; - QString runtime_export_output_name_; - QString runtime_export_destination_directory_; - QString runtime_export_package_directory_; - QString runtime_export_process_output_; - bool runtime_export_completion_handled_ = false; - struct HmiClipboardData { std::vector controls; diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index a1cd086..34c2c06 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -207,7 +207,26 @@ + + + 数据监控 + + + + 8 + + + 8 + + + 8 + + + 8 + + + @@ -906,28 +925,6 @@ - - - - 640 - 220 - - - - 数据监控 - - - 2 - - - - 6 - 6 - 6 - 6 - - - diff --git a/app/tests/runtime_project_bundle_tests.cpp b/app/tests/runtime_project_bundle_tests.cpp new file mode 100644 index 0000000..1efc747 --- /dev/null +++ b/app/tests/runtime_project_bundle_tests.cpp @@ -0,0 +1,84 @@ +#include "infrastructure/runtime_project_bundle.h" +#include "support/test_support.h" + +#include +#include + +#include +#include + +namespace { + +using TestSupport::require; + +void writeFile(const QString &path, const QByteArray &bytes) +{ + QFile file(path); + require(file.open(QIODevice::WriteOnly), "test file must open for writing"); + require(file.write(bytes) == bytes.size(), "test file must be written fully"); +} + +void testBundleRoundTripAndValidation() +{ + QTemporaryDir directory; + require(directory.isValid(), "temporary directory must be available"); + + const QString template_path = directory.filePath(QStringLiteral("template.exe")); + const QString project_path = directory.filePath(QStringLiteral("project.json")); + const QString bundle_path = directory.filePath(QStringLiteral("motor.exe")); + const QByteArray template_bytes("MZ-template-binary"); + const QByteArray project_bytes( + "{\"formatVersion\":\"1.0\",\"name\":\"motor\"}"); + writeFile(template_path, template_bytes); + writeFile(project_path, project_bytes); + + const RuntimeProjectBundleWriteResult write_result = + RuntimeProjectBundleService::write( + template_path, project_path, bundle_path); + require(write_result.succeeded, "runtime bundle must be written"); + + const RuntimeProjectBundleLoadResult loaded = + RuntimeProjectBundleService::load(bundle_path); + require(loaded.status == RuntimeProjectBundleStatus::Loaded, + "runtime bundle must be recognized"); + require(loaded.project_data == project_bytes, + "runtime bundle must preserve project bytes"); + + const RuntimeProjectBundleLoadResult ordinary = + RuntimeProjectBundleService::load(template_path); + require(ordinary.status == RuntimeProjectBundleStatus::NotFound, + "ordinary executable must not be treated as a runtime bundle"); + + QFile corrupted(bundle_path); + require(corrupted.open(QIODevice::ReadWrite), + "bundle must open for corruption regression"); + require(corrupted.seek(corrupted.size() - 1), + "bundle footer must be seekable"); + const char invalid_byte = static_cast(0xff); + require(corrupted.write(&invalid_byte, 1) == 1, + "bundle corruption must be writable"); + corrupted.close(); + const RuntimeProjectBundleLoadResult invalid = + RuntimeProjectBundleService::load(bundle_path); + require(invalid.status == RuntimeProjectBundleStatus::Invalid, + "corrupted runtime bundle must be rejected"); +} + +} // namespace + +int main() +{ + try + { + testBundleRoundTripAndValidation(); + } + catch (const std::exception &error) + { + std::cerr << "runtime project bundle tests failed: " + << error.what() << '\n'; + return 1; + } + + std::cout << "runtime project bundle tests passed\n"; + return 0; +} diff --git a/app/tests/runtime_project_bundle_tests.pro b/app/tests/runtime_project_bundle_tests.pro new file mode 100644 index 0000000..e35a061 --- /dev/null +++ b/app/tests/runtime_project_bundle_tests.pro @@ -0,0 +1,13 @@ +include(pri/test_defaults.pri) + +TARGET = runtime_project_bundle_tests +QT += core +CONFIG += testcase + +SOURCES += \ + runtime_project_bundle_tests.cpp \ + ../src/infrastructure/runtime_project_bundle.cpp + +HEADERS += \ + ../src/infrastructure/runtime_project_bundle.h \ + $$TEST_SUPPORT_HEADERS diff --git a/app/tests/tests.pro b/app/tests/tests.pro index c17f4d7..ed3cdb9 100644 --- a/app/tests/tests.pro +++ b/app/tests/tests.pro @@ -11,6 +11,7 @@ SUBDIRS += \ offline_simulation \ project_management \ register_monitor \ + runtime_project_bundle \ runtime_mode \ runtime_panel_controller \ plc_connection_dialog \ @@ -24,6 +25,7 @@ logic_editor_service.file = logic_editor_service_tests.pro offline_simulation.file = offline_simulation_service_tests.pro project_management.file = project_management_tests.pro register_monitor.file = register_monitor_service_tests.pro +runtime_project_bundle.file = runtime_project_bundle_tests.pro runtime_mode.file = runtime_mode_service_tests.pro runtime_panel_controller.file = runtime_panel_controller_tests.pro plc_connection_dialog.file = plc_connection_dialog_tests.pro diff --git a/scripts/run_qt_tests.ps1 b/scripts/run_qt_tests.ps1 index e803f2f..a41ce5b 100644 --- a/scripts/run_qt_tests.ps1 +++ b/scripts/run_qt_tests.ps1 @@ -33,6 +33,7 @@ $functionalTargets = @( 'offline_simulation_service_tests', 'project_management_tests', 'register_monitor_service_tests', + 'runtime_project_bundle_tests', 'runtime_mode_service_tests', 'runtime_panel_controller_tests', 'plc_connection_dialog_tests',