| @@ -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 | |||
| @@ -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_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( | |||
| @@ -25,6 +25,7 @@ | |||
| #include "ui_main_window.h" | |||
| #include "domain/project_limits.h" | |||
| #include "infrastructure/runtime_project_bundle.h" | |||
| #include <QActionGroup> | |||
| #include <QAbstractSpinBox> | |||
| @@ -51,11 +52,9 @@ | |||
| #include <QToolButton> | |||
| #include <QTextEdit> | |||
| #include <QTimer> | |||
| #include <QProcess> | |||
| #include <QDir> | |||
| #include <QFileInfo> | |||
| #include <QProgressDialog> | |||
| #include <QDateTime> | |||
| #include <algorithm> | |||
| @@ -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>( | |||
| 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<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); | |||
| 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( | |||
| @@ -15,7 +15,6 @@ | |||
| #include "services/application_settings.h" | |||
| #include <QMainWindow> | |||
| #include <QProcess> | |||
| #include <memory> | |||
| #include <variant> | |||
| @@ -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<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 | |||
| { | |||
| std::vector<HmiControl> controls; | |||
| @@ -207,7 +207,26 @@ | |||
| </item> | |||
| </layout> | |||
| </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> | |||
| </item> | |||
| </layout> | |||
| </widget> | |||
| @@ -906,28 +925,6 @@ | |||
| </layout> | |||
| </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"> | |||
| <property name="minimumSize"> | |||
| <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 \ | |||
| 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 | |||
| @@ -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', | |||