| @@ -59,6 +59,7 @@ SOURCES += \ | |||||
| src/infrastructure/plc_communication_service.cpp \ | src/infrastructure/plc_communication_service.cpp \ | ||||
| src/infrastructure/plc_discovery_service.cpp \ | src/infrastructure/plc_discovery_service.cpp \ | ||||
| src/infrastructure/application_settings_loader.cpp \ | src/infrastructure/application_settings_loader.cpp \ | ||||
| src/infrastructure/runtime_project_bundle.cpp \ | |||||
| src/infrastructure/json_project_storage.cpp \ | src/infrastructure/json_project_storage.cpp \ | ||||
| src/ui/hmi_editor_widget.cpp \ | src/ui/hmi_editor_widget.cpp \ | ||||
| src/ui/logic_editor_widget.cpp | src/ui/logic_editor_widget.cpp | ||||
| @@ -113,6 +114,7 @@ HEADERS += \ | |||||
| src/infrastructure/plc_communication_service.h \ | src/infrastructure/plc_communication_service.h \ | ||||
| src/infrastructure/plc_discovery_service.h \ | src/infrastructure/plc_discovery_service.h \ | ||||
| src/infrastructure/application_settings_loader.h \ | src/infrastructure/application_settings_loader.h \ | ||||
| src/infrastructure/runtime_project_bundle.h \ | |||||
| src/infrastructure/json_project_storage.h \ | src/infrastructure/json_project_storage.h \ | ||||
| src/ui/hmi_editor_widget.h \ | src/ui/hmi_editor_widget.h \ | ||||
| src/ui/logic_editor_widget.h | src/ui/logic_editor_widget.h | ||||
| @@ -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 <QCryptographicHash> | |||||
| #include <QFile> | |||||
| #include <QFileInfo> | |||||
| #include <QObject> | |||||
| #include <limits> | |||||
| 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<char>((value >> shift) & 0xff)); | |||||
| } | |||||
| } | |||||
| void appendUint64LittleEndian(QByteArray *bytes, quint64 value) | |||||
| { | |||||
| for (int shift = 0; shift < 64; shift += 8) | |||||
| { | |||||
| bytes->append(static_cast<char>((value >> shift) & 0xff)); | |||||
| } | |||||
| } | |||||
| quint32 readUint32LittleEndian(const char *bytes) | |||||
| { | |||||
| quint32 value = 0; | |||||
| for (int shift = 0; shift < 32; shift += 8) | |||||
| { | |||||
| value |= static_cast<quint32>( | |||||
| static_cast<unsigned char>(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<quint64>( | |||||
| static_cast<unsigned char>(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<quint64>(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<quint64>( | |||||
| executable_size - static_cast<qint64>(kFooterSize)) | |||||
| || project_size > static_cast<quint64>(std::numeric_limits<qint64>::max())) | |||||
| { | |||||
| return invalidBundle(QObject::tr("用户运行程序工程数据长度无效")); | |||||
| } | |||||
| const qint64 project_offset = executable_size - kFooterSize | |||||
| - static_cast<qint64>(project_size); | |||||
| if (!executable.seek(project_offset)) | |||||
| { | |||||
| return invalidBundle(QObject::tr("无法读取用户运行程序工程数据")); | |||||
| } | |||||
| const QByteArray project_data = executable.read( | |||||
| static_cast<qint64>(project_size)); | |||||
| if (project_data.size() != static_cast<qint64>(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, {}}; | |||||
| } | |||||
| @@ -0,0 +1,50 @@ | |||||
| /** | |||||
| * @file runtime_project_bundle.h | |||||
| * @brief 定义用户运行程序工程数据封装服务 | |||||
| * @version 1.0.0 | |||||
| * @author suyu | |||||
| * @date 2026-08-26 | |||||
| */ | |||||
| #pragma once | |||||
| #include <QByteArray> | |||||
| #include <QString> | |||||
| /** 用户运行程序工程数据的读取状态 */ | |||||
| 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); | |||||
| }; | |||||
| @@ -17,6 +17,7 @@ | |||||
| #include "infrastructure/plc_communication_service.h" | #include "infrastructure/plc_communication_service.h" | ||||
| #include "infrastructure/plc_discovery_service.h" | #include "infrastructure/plc_discovery_service.h" | ||||
| #include "infrastructure/plc_register_repository.h" | #include "infrastructure/plc_register_repository.h" | ||||
| #include "infrastructure/runtime_project_bundle.h" | |||||
| #include "domain/active_register_repository.h" | #include "domain/active_register_repository.h" | ||||
| #include "domain/virtual_register_repository.h" | #include "domain/virtual_register_repository.h" | ||||
| #include "services/hmi_editor_service.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); | runtime_project_path = arguments.at(project_option + 1); | ||||
| } | } | ||||
| const QFileInfo executable_info(application.applicationFilePath()); | const QFileInfo executable_info(application.applicationFilePath()); | ||||
| const RuntimeProjectBundleLoadResult bundled_project_result = | |||||
| RuntimeProjectBundleService::load(executable_info.absoluteFilePath()); | |||||
| const QString bundled_project = executable_info.absolutePath() | const QString bundled_project = executable_info.absolutePath() | ||||
| + QStringLiteral("/runtime-project.json"); | + QStringLiteral("/runtime-project.json"); | ||||
| if (runtime_project_path.isEmpty() | if (runtime_project_path.isEmpty() | ||||
| @@ -63,8 +66,21 @@ int main(int argc, char *argv[]) | |||||
| } | } | ||||
| const bool embedded_runtime_project = QFile::exists( | const bool embedded_runtime_project = QFile::exists( | ||||
| QStringLiteral(":/runtime-project.json")); | 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() | const bool user_runtime_mode = !runtime_project_path.isEmpty() | ||||
| || embedded_runtime_project; | |||||
| || embedded_runtime_project | |||||
| || bundled_runtime_project; | |||||
| const ApplicationSettingsLoadResult application_settings = | const ApplicationSettingsLoadResult application_settings = | ||||
| ApplicationSettingsLoader::load( | ApplicationSettingsLoader::load( | ||||
| @@ -102,6 +118,25 @@ int main(int argc, char *argv[]) | |||||
| result = project_service.load(temporary_path.toUtf8().toStdString()); | result = project_service.load(temporary_path.toUtf8().toStdString()); | ||||
| QFile::remove(temporary_path); | 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 | else | ||||
| { | { | ||||
| result = project_service.load( | result = project_service.load( | ||||
| @@ -25,6 +25,7 @@ | |||||
| #include "ui_main_window.h" | #include "ui_main_window.h" | ||||
| #include "domain/project_limits.h" | #include "domain/project_limits.h" | ||||
| #include "infrastructure/runtime_project_bundle.h" | |||||
| #include <QActionGroup> | #include <QActionGroup> | ||||
| #include <QAbstractSpinBox> | #include <QAbstractSpinBox> | ||||
| @@ -51,11 +52,9 @@ | |||||
| #include <QToolButton> | #include <QToolButton> | ||||
| #include <QTextEdit> | #include <QTextEdit> | ||||
| #include <QTimer> | #include <QTimer> | ||||
| #include <QProcess> | |||||
| #include <QDir> | #include <QDir> | ||||
| #include <QFileInfo> | #include <QFileInfo> | ||||
| #include <QProgressDialog> | #include <QProgressDialog> | ||||
| #include <QDateTime> | |||||
| #include <algorithm> | #include <algorithm> | ||||
| @@ -469,7 +468,6 @@ void MainWindow::initializeUserRuntime() | |||||
| ui_->hmiToolBar->setVisible(false); | ui_->hmiToolBar->setVisible(false); | ||||
| ui_->logicToolBar->setVisible(false); | ui_->logicToolBar->setVisible(false); | ||||
| ui_->outputDock->setVisible(false); | ui_->outputDock->setVisible(false); | ||||
| ui_->dataMonitorDock->setVisible(false); | |||||
| ui_->projectDock->setVisible(false); | ui_->projectDock->setVisible(false); | ||||
| ui_->propertiesDock->setVisible(false); | ui_->propertiesDock->setVisible(false); | ||||
| QTimer::singleShot( | QTimer::singleShot( | ||||
| @@ -537,16 +535,6 @@ void MainWindow::requestUserRuntimeMode(ApplicationMode requested_mode) | |||||
| MainWindow::~MainWindow() | 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); | qApp->removeEventFilter(this); | ||||
| register_monitor_service_.setAddressesChangedCallback({}); | register_monitor_service_.setAddressesChangedCallback({}); | ||||
| runtime_mode_service_.setPlcStatusChangedCallback({}); | runtime_mode_service_.setPlcStatusChangedCallback({}); | ||||
| @@ -569,16 +557,6 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) | |||||
| void MainWindow::closeEvent(QCloseEvent *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()) | if (!confirmSaveBeforeDestructiveAction()) | ||||
| { | { | ||||
| event->ignore(); | event->ignore(); | ||||
| @@ -879,6 +857,11 @@ void MainWindow::configureActions() | |||||
| { | { | ||||
| showLogicNodeProperties(selected_logic_node_id_); | showLogicNodeProperties(selected_logic_node_id_); | ||||
| } | } | ||||
| else | |||||
| { | |||||
| // 数据监控页不对应 HMI 或梯形图对象 | |||||
| showControlProperties({}); | |||||
| } | |||||
| if (index == 0 || index == 1) | if (index == 0 || index == 1) | ||||
| { | { | ||||
| refreshProjectUi(); | refreshProjectUi(); | ||||
| @@ -1057,12 +1040,10 @@ void MainWindow::configureRuntimeMonitor() | |||||
| void MainWindow::configureDataMonitor() | void MainWindow::configureDataMonitor() | ||||
| { | { | ||||
| data_monitor_dock_ = ui_->dataMonitorDock; | |||||
| data_monitor_widget_ = new FreeMonitorWidget( | data_monitor_widget_ = new FreeMonitorWidget( | ||||
| register_monitor_service_, data_monitor_dock_); | |||||
| register_monitor_service_, ui_->dataMonitorTab); | |||||
| data_monitor_widget_->setObjectName(QStringLiteral("dataMonitorWidget")); | 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, | connect(data_monitor_widget_, &FreeMonitorWidget::monitorAddressesChanged, | ||||
| this, | this, | ||||
| @@ -1120,10 +1101,6 @@ void MainWindow::refreshDataMonitorUi() | |||||
| || online_connected); | || online_connected); | ||||
| data_monitor_widget_->refreshValues( | data_monitor_widget_->refreshValues( | ||||
| mode, runtime_mode_service_.plcConnectionState()); | mode, runtime_mode_service_.plcConnectionState()); | ||||
| if (data_monitor_dock_ != nullptr) | |||||
| { | |||||
| data_monitor_dock_->setVisible(mode == ApplicationMode::Editing); | |||||
| } | |||||
| } | } | ||||
| void MainWindow::configureProjectTree() | void MainWindow::configureProjectTree() | ||||
| @@ -1925,15 +1902,6 @@ void MainWindow::loadProject() | |||||
| void MainWindow::exportRuntimeProgram() | void MainWindow::exportRuntimeProgram() | ||||
| { | { | ||||
| if (runtime_export_process_ != nullptr) | |||||
| { | |||||
| showProjectResult( | |||||
| tr("导出用户运行程序"), | |||||
| tr("已有一个导出任务正在执行,请等待完成"), | |||||
| false); | |||||
| return; | |||||
| } | |||||
| std::string validation_error; | std::string validation_error; | ||||
| if (!project_service_.project().validateForRunning( | if (!project_service_.project().validateForRunning( | ||||
| project_service_.projectLimits(), &validation_error)) | project_service_.projectLimits(), &validation_error)) | ||||
| @@ -2006,8 +1974,6 @@ void MainWindow::exportRuntimeProgram() | |||||
| const QString temp_project = QDir::tempPath() | const QString temp_project = QDir::tempPath() | ||||
| + QStringLiteral("/qtproxinje-runtime-") | + QStringLiteral("/qtproxinje-runtime-") | ||||
| + QString::number(QCoreApplication::applicationPid()) | + QString::number(QCoreApplication::applicationPid()) | ||||
| + QStringLiteral("-") | |||||
| + QString::number(QDateTime::currentMSecsSinceEpoch()) | |||||
| + QStringLiteral(".json"); | + QStringLiteral(".json"); | ||||
| const ProjectOperationResult save_result = project_service_.exportAs( | const ProjectOperationResult save_result = project_service_.exportAs( | ||||
| temp_project.toUtf8().toStdString()); | temp_project.toUtf8().toStdString()); | ||||
| @@ -2017,181 +1983,115 @@ void MainWindow::exportRuntimeProgram() | |||||
| return; | 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>( | |||||
| QProgressDialog progress( | |||||
| tr("正在准备导出用户运行程序…"), | tr("正在准备导出用户运行程序…"), | ||||
| QString(), | QString(), | ||||
| 0, | 0, | ||||
| 100, | 100, | ||||
| this); | 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<QProcess>(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<int, QProcess::ExitStatus>(&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); | 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; | 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; | 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); | 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( | ui_->exportRuntimeAction->setEnabled( | ||||
| runtime_mode_service_.policy().allowsProjectEditing); | runtime_mode_service_.policy().allowsProjectEditing); | ||||
| showProjectResult(tr("导出用户运行程序"), result_message, succeeded); | |||||
| showProjectResult( | |||||
| tr("导出用户运行程序"), | |||||
| tr("已导出:%1").arg(destination_executable), | |||||
| true); | |||||
| } | } | ||||
| void MainWindow::showProjectResult( | void MainWindow::showProjectResult( | ||||
| @@ -15,7 +15,6 @@ | |||||
| #include "services/application_settings.h" | #include "services/application_settings.h" | ||||
| #include <QMainWindow> | #include <QMainWindow> | ||||
| #include <QProcess> | |||||
| #include <memory> | #include <memory> | ||||
| #include <variant> | #include <variant> | ||||
| @@ -27,9 +26,7 @@ class QActionGroup; | |||||
| class QCloseEvent; | class QCloseEvent; | ||||
| class QEvent; | class QEvent; | ||||
| class QLabel; | class QLabel; | ||||
| class QDockWidget; | |||||
| class QTimer; | class QTimer; | ||||
| class QProgressDialog; | |||||
| namespace Ui { | namespace Ui { | ||||
| class MainWindow; | class MainWindow; | ||||
| @@ -134,7 +131,7 @@ private: | |||||
| void configureLogicEditor(); | void configureLogicEditor(); | ||||
| /** 创建并连接运行监控面板 */ | /** 创建并连接运行监控面板 */ | ||||
| void configureRuntimeMonitor(); | void configureRuntimeMonitor(); | ||||
| /** 创建并连接编辑态数据监控面板 */ | |||||
| /** 创建并连接数据监控编辑页 */ | |||||
| void configureDataMonitor(); | void configureDataMonitor(); | ||||
| /** 创建并连接工程树 */ | /** 创建并连接工程树 */ | ||||
| void configureProjectTree(); | void configureProjectTree(); | ||||
| @@ -216,10 +213,6 @@ private: | |||||
| void loadProject(); | void loadProject(); | ||||
| /** 将当前工程构建为用户可直接运行的专用 exe */ | /** 将当前工程构建为用户可直接运行的专用 exe */ | ||||
| void exportRuntimeProgram(); | void exportRuntimeProgram(); | ||||
| /** 更新用户运行程序导出的阶段进度 */ | |||||
| void handleRuntimeExportOutput(); | |||||
| /** 完成用户运行程序导出并复制最终目录 */ | |||||
| void handleRuntimeExportFinished(int exit_code, QProcess::ExitStatus exit_status); | |||||
| /** 在状态栏和输出面板显示工程操作结果 */ | /** 在状态栏和输出面板显示工程操作结果 */ | ||||
| void showProjectResult(const QString &action, const QString &message, bool succeeded); | void showProjectResult(const QString &action, const QString &message, bool succeeded); | ||||
| /** 向输出面板追加一条消息 */ | /** 向输出面板追加一条消息 */ | ||||
| @@ -302,8 +295,7 @@ private: | |||||
| LogicEditorWidget *logic_editor_widget_ = nullptr; | LogicEditorWidget *logic_editor_widget_ = nullptr; | ||||
| /** 唯一的运行监控控件 */ | /** 唯一的运行监控控件 */ | ||||
| RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | ||||
| /** 主窗口中的数据监控面板 */ | |||||
| QDockWidget *data_monitor_dock_ = nullptr; | |||||
| /** 主窗口中的数据监控编辑页 */ | |||||
| FreeMonitorWidget *data_monitor_widget_ = nullptr; | FreeMonitorWidget *data_monitor_widget_ = nullptr; | ||||
| QTimer *data_monitor_refresh_timer_ = nullptr; | QTimer *data_monitor_refresh_timer_ = nullptr; | ||||
| /** 显示当前运行模式的状态标签 */ | /** 显示当前运行模式的状态标签 */ | ||||
| @@ -324,16 +316,6 @@ private: | |||||
| std::string current_logic_id_; | std::string current_logic_id_; | ||||
| /** 是否已经安排了待处理的 PLC 状态刷新 */ | /** 是否已经安排了待处理的 PLC 状态刷新 */ | ||||
| bool plc_status_update_pending_ = false; | bool plc_status_update_pending_ = false; | ||||
| /** 用户运行程序导出进程及其临时状态 */ | |||||
| std::unique_ptr<QProcess> runtime_export_process_; | |||||
| std::unique_ptr<QProgressDialog> 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 | struct HmiClipboardData | ||||
| { | { | ||||
| std::vector<HmiControl> controls; | std::vector<HmiControl> controls; | ||||
| @@ -207,7 +207,26 @@ | |||||
| </item> | </item> | ||||
| </layout> | </layout> | ||||
| </widget> | </widget> | ||||
| <widget class="QWidget" name="dataMonitorTab"> | |||||
| <attribute name="title"> | |||||
| <string>数据监控</string> | |||||
| </attribute> | |||||
| <layout class="QVBoxLayout" name="dataMonitorLayout"> | |||||
| <property name="leftMargin"> | |||||
| <number>8</number> | |||||
| </property> | |||||
| <property name="topMargin"> | |||||
| <number>8</number> | |||||
| </property> | |||||
| <property name="rightMargin"> | |||||
| <number>8</number> | |||||
| </property> | |||||
| <property name="bottomMargin"> | |||||
| <number>8</number> | |||||
| </property> | |||||
| </layout> | |||||
| </widget> | </widget> | ||||
| </widget> | |||||
| </item> | </item> | ||||
| </layout> | </layout> | ||||
| </widget> | </widget> | ||||
| @@ -906,28 +925,6 @@ | |||||
| </layout> | </layout> | ||||
| </widget> | </widget> | ||||
| </widget> | </widget> | ||||
| <widget class="QDockWidget" name="dataMonitorDock"> | |||||
| <property name="minimumSize"> | |||||
| <size> | |||||
| <width>640</width> | |||||
| <height>220</height> | |||||
| </size> | |||||
| </property> | |||||
| <property name="windowTitle"> | |||||
| <string>数据监控</string> | |||||
| </property> | |||||
| <attribute name="dockWidgetArea"> | |||||
| <number>2</number> | |||||
| </attribute> | |||||
| <widget class="QWidget" name="dataMonitorDockContents"> | |||||
| <layout class="QVBoxLayout" name="dataMonitorDockLayout"> | |||||
| <property name="leftMargin"><number>6</number></property> | |||||
| <property name="topMargin"><number>6</number></property> | |||||
| <property name="rightMargin"><number>6</number></property> | |||||
| <property name="bottomMargin"><number>6</number></property> | |||||
| </layout> | |||||
| </widget> | |||||
| </widget> | |||||
| <widget class="QDockWidget" name="outputDock"> | <widget class="QDockWidget" name="outputDock"> | ||||
| <property name="minimumSize"> | <property name="minimumSize"> | ||||
| <size> | <size> | ||||
| @@ -0,0 +1,84 @@ | |||||
| #include "infrastructure/runtime_project_bundle.h" | |||||
| #include "support/test_support.h" | |||||
| #include <QFile> | |||||
| #include <QTemporaryDir> | |||||
| #include <iostream> | |||||
| #include <stdexcept> | |||||
| 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<char>(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; | |||||
| } | |||||
| @@ -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 | |||||
| @@ -11,6 +11,7 @@ SUBDIRS += \ | |||||
| offline_simulation \ | offline_simulation \ | ||||
| project_management \ | project_management \ | ||||
| register_monitor \ | register_monitor \ | ||||
| runtime_project_bundle \ | |||||
| runtime_mode \ | runtime_mode \ | ||||
| runtime_panel_controller \ | runtime_panel_controller \ | ||||
| plc_connection_dialog \ | plc_connection_dialog \ | ||||
| @@ -24,6 +25,7 @@ logic_editor_service.file = logic_editor_service_tests.pro | |||||
| offline_simulation.file = offline_simulation_service_tests.pro | offline_simulation.file = offline_simulation_service_tests.pro | ||||
| project_management.file = project_management_tests.pro | project_management.file = project_management_tests.pro | ||||
| register_monitor.file = register_monitor_service_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_mode.file = runtime_mode_service_tests.pro | ||||
| runtime_panel_controller.file = runtime_panel_controller_tests.pro | runtime_panel_controller.file = runtime_panel_controller_tests.pro | ||||
| plc_connection_dialog.file = plc_connection_dialog_tests.pro | plc_connection_dialog.file = plc_connection_dialog_tests.pro | ||||
| @@ -33,6 +33,7 @@ $functionalTargets = @( | |||||
| 'offline_simulation_service_tests', | 'offline_simulation_service_tests', | ||||
| 'project_management_tests', | 'project_management_tests', | ||||
| 'register_monitor_service_tests', | 'register_monitor_service_tests', | ||||
| 'runtime_project_bundle_tests', | |||||
| 'runtime_mode_service_tests', | 'runtime_mode_service_tests', | ||||
| 'runtime_panel_controller_tests', | 'runtime_panel_controller_tests', | ||||
| 'plc_connection_dialog_tests', | 'plc_connection_dialog_tests', | ||||