From 985468a7a7914ce912b2f093ecac8a13dded3b75 Mon Sep 17 00:00:00 2001 From: Cacodemon345 Date: Fri, 8 Aug 2025 23:50:54 +0600 Subject: [PATCH 1/3] Implement VM cloning option --- src/qt/qt_vmmanager_main.cpp | 139 +++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/qt/qt_vmmanager_main.cpp b/src/qt/qt_vmmanager_main.cpp index b55e2be2a..c49a54f16 100644 --- a/src/qt/qt_vmmanager_main.cpp +++ b/src/qt/qt_vmmanager_main.cpp @@ -15,6 +15,8 @@ * Copyright 2024 cold-brewed */ +#include +#include #include #include #include @@ -23,6 +25,11 @@ #include #include #include +#include + +#include +#include +#include #include "qt_vmmanager_main.hpp" #include "ui_qt_vmmanager_main.h" @@ -96,6 +103,138 @@ VMManagerMain::VMManagerMain(QWidget *parent) : }); setSystemIcon.setEnabled(!selected_sysconfig->window_obscured); + QAction cloneMachine(tr("&Clone...")); + contextMenu.addAction(&cloneMachine); + connect(&cloneMachine, &QAction::triggered, [this] { + QDialog dialog = QDialog(this); + auto layout = new QVBoxLayout(&dialog); + layout->setSizeConstraint(QLayout::SetFixedSize); + layout->addWidget(new QLabel(tr("Virtual machine \"%1\" (%2) will be cloned into:").arg(selected_sysconfig->displayName, selected_sysconfig->config_dir))); + QLineEdit* edit = new QLineEdit(&dialog); + layout->addWidget(edit); + QLabel* errLabel = new QLabel(&dialog); + layout->addWidget(errLabel); + errLabel->setVisible(false); + QDialogButtonBox* buttonBox = new QDialogButtonBox(&dialog); + buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true); + connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + connect(edit, &QLineEdit::textChanged, this, [this, errLabel, buttonBox] (const QString& text) { + bool isSpaceOnly = true; +#ifdef Q_OS_WINDOWS + const char illegalChars[] = "<>:\"|?*\\/"; +#else + const char illegalChars[] = "\\/"; +#endif + for (const auto& curChar : text) { + for (int i = 0; i < sizeof(illegalChars) - 1; i++) { + if (illegalChars[i] == curChar) { + goto illegal_chars; + } + if (!curChar.isSpace()) { + isSpaceOnly = false; + } + } + } + errLabel->setVisible(false); + buttonBox->button(QDialogButtonBox::Ok)->setDisabled(isSpaceOnly || text.isEmpty()); + if (QDir((QString(vmm_path) + "/") + text).exists() && buttonBox->button(QDialogButtonBox::Ok)->isEnabled()) { + goto dir_already_exists; + } + return; +dir_already_exists: + errLabel->setText(tr("Directory %1 already exists").arg(QDir((QString(vmm_path) + "/") + text).canonicalPath())); + errLabel->setVisible(true); + buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true); + return; +illegal_chars: + QString illegalCharsDisplay; + for (int i = 0; i < sizeof(illegalChars) - 1; i++) { + illegalCharsDisplay.push_back(illegalChars[i]); + illegalCharsDisplay.push_back(' '); + } + illegalCharsDisplay.chop(1); + errLabel->setText(tr("You cannot use the following characters in the name: %1").arg(illegalCharsDisplay)); + errLabel->setVisible(true); + buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true); + return; + }); + + if (dialog.exec() > 0) { + std::atomic_bool finished{false}; + std::atomic_int errCode; + auto vmDir = QDir(vmm_path).canonicalPath(); + vmDir.append("/"); + vmDir.append(edit->text()); + vmDir.append("/"); + + if (!QDir(vmDir).mkpath(".")) { + QMessageBox::critical(this, tr("Clone"), tr("Failed to create directory for cloned VM"), QMessageBox::Ok); + return; + } + + QProgressDialog* progDialog = new QProgressDialog(this); + progDialog->setMaximum(0); + progDialog->setMinimum(0); + progDialog->setWindowFlags(progDialog->windowFlags() & ~Qt::WindowCloseButtonHint); + progDialog->setFixedSize(progDialog->sizeHint()); + progDialog->setMinimumDuration(0); + progDialog->setCancelButton(nullptr); + progDialog->setAutoClose(false); + progDialog->setAutoReset(false); + progDialog->setAttribute(Qt::WA_DeleteOnClose, true); + progDialog->setValue(0); + progDialog->show(); +#ifdef _WIN32 + std::filesystem::path srcPath(selected_sysconfig->config_dir.toStdWString().c_str()); + std::filesystem::path dstPath(vmDir.toStdWString().c_str()); +#else + std::filesystem::path srcPath(selected_sysconfig->config_dir.toUtf8().data()); + std::filesystem::path dstPath(vmDir.toUtf8().data()); +#endif + std::thread copyThread([this, &finished, srcPath, dstPath, &errCode] { + std::error_code code; + code.clear(); + std::filesystem::copy(srcPath, dstPath, std::filesystem::copy_options::update_existing | std::filesystem::copy_options::recursive, code); + errCode = code.value(); + finished = true; + }); + while (!finished) { + QApplication::processEvents(); + } + copyThread.join(); + progDialog->close(); + if (errCode) { + std::filesystem::remove_all(dstPath); + QMessageBox::critical(this, tr("Clone"), tr("Failed to clone VM: %1").arg(errCode), QMessageBox::Ok); + return; + } + + QFileInfo configFileInfo(vmDir + CONFIG_FILE); + if (configFileInfo.exists()) { + const auto current_index = ui->listView->currentIndex(); + vm_model->reload(this); + const auto created_object = vm_model->getIndexForConfigFile(configFileInfo); + if (created_object.row() < 0) { + // For some reason the index of the new object couldn't be determined. Fall back to the old index. + ui->listView->setCurrentIndex(current_index); + return; + } + auto added_system = vm_model->getConfigObjectForIndex(created_object); + added_system->setDisplayName(edit->text()); + // Get the index of the newly-created system and select it + const QModelIndex mapped_index = proxy_model->mapFromSource(created_object); + ui->listView->setCurrentIndex(mapped_index); + } else { + std::filesystem::remove_all(dstPath); + QMessageBox::critical(this, tr("Clone"), tr("Failed to clone VM for an unknown reason."), QMessageBox::Ok); + return; + } + } + }); + QAction killIcon(tr("&Kill")); contextMenu.addAction(&killIcon); connect(&killIcon, &QAction::triggered, [this, parent] { From f486e905a188f2b04a231a07148eb4337e75d479 Mon Sep 17 00:00:00 2001 From: Cacodemon345 Date: Sat, 9 Aug 2025 00:28:38 +0600 Subject: [PATCH 2/3] Fix ampersand position --- src/qt/qt_vmmanager_main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qt/qt_vmmanager_main.cpp b/src/qt/qt_vmmanager_main.cpp index c49a54f16..ea1d0e2a5 100644 --- a/src/qt/qt_vmmanager_main.cpp +++ b/src/qt/qt_vmmanager_main.cpp @@ -103,7 +103,7 @@ VMManagerMain::VMManagerMain(QWidget *parent) : }); setSystemIcon.setEnabled(!selected_sysconfig->window_obscured); - QAction cloneMachine(tr("&Clone...")); + QAction cloneMachine(tr("C&lone...")); contextMenu.addAction(&cloneMachine); connect(&cloneMachine, &QAction::triggered, [this] { QDialog dialog = QDialog(this); From d279a6e36d09cf728de48c4a05190971f9191ce3 Mon Sep 17 00:00:00 2001 From: Cacodemon345 Date: Sun, 10 Aug 2025 00:44:03 +0600 Subject: [PATCH 3/3] Alternate method of VM cloning --- src/qt/qt_vmmanager_main.cpp | 68 ++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/src/qt/qt_vmmanager_main.cpp b/src/qt/qt_vmmanager_main.cpp index ea1d0e2a5..e0a903c58 100644 --- a/src/qt/qt_vmmanager_main.cpp +++ b/src/qt/qt_vmmanager_main.cpp @@ -27,7 +27,6 @@ #include #include -#include #include #include @@ -36,6 +35,54 @@ #include "qt_vmmanager_model.hpp" #include "qt_vmmanager_addmachine.hpp" +// https://stackoverflow.com/a/36460740 +bool copyPath(QString sourceDir, QString destinationDir, bool overWriteDirectory) +{ + QDir originDirectory(sourceDir); + + if (! originDirectory.exists()) + { + return false; + } + + QDir destinationDirectory(destinationDir); + + if(destinationDirectory.exists() && !overWriteDirectory) + { + return false; + } + else if(destinationDirectory.exists() && overWriteDirectory) + { + destinationDirectory.removeRecursively(); + } + + originDirectory.mkpath(destinationDir); + + foreach (QString directoryName, originDirectory.entryList(QDir::Dirs | \ + QDir::NoDotAndDotDot)) + { + QString destinationPath = destinationDir + "/" + directoryName; + originDirectory.mkpath(destinationPath); + copyPath(sourceDir + "/" + directoryName, destinationPath, overWriteDirectory); + } + + foreach (QString fileName, originDirectory.entryList(QDir::Files)) + { + QFile::copy(sourceDir + "/" + fileName, destinationDir + "/" + fileName); + } + + /*! Possible race-condition mitigation? */ + QDir finalDestination(destinationDir); + finalDestination.refresh(); + + if(finalDestination.exists()) + { + return true; + } + + return false; +} + VMManagerMain::VMManagerMain(QWidget *parent) : QWidget(parent), ui(new Ui::VMManagerMain), selected_sysconfig(new VMManagerSystem) { ui->setupUi(this); @@ -187,18 +234,11 @@ illegal_chars: progDialog->setAttribute(Qt::WA_DeleteOnClose, true); progDialog->setValue(0); progDialog->show(); -#ifdef _WIN32 - std::filesystem::path srcPath(selected_sysconfig->config_dir.toStdWString().c_str()); - std::filesystem::path dstPath(vmDir.toStdWString().c_str()); -#else - std::filesystem::path srcPath(selected_sysconfig->config_dir.toUtf8().data()); - std::filesystem::path dstPath(vmDir.toUtf8().data()); -#endif + QString srcPath = selected_sysconfig->config_dir; + QString dstPath = vmDir; + std::thread copyThread([this, &finished, srcPath, dstPath, &errCode] { - std::error_code code; - code.clear(); - std::filesystem::copy(srcPath, dstPath, std::filesystem::copy_options::update_existing | std::filesystem::copy_options::recursive, code); - errCode = code.value(); + errCode = copyPath(srcPath, dstPath, true); finished = true; }); while (!finished) { @@ -207,7 +247,7 @@ illegal_chars: copyThread.join(); progDialog->close(); if (errCode) { - std::filesystem::remove_all(dstPath); + QDir(dstPath).removeRecursively(); QMessageBox::critical(this, tr("Clone"), tr("Failed to clone VM: %1").arg(errCode), QMessageBox::Ok); return; } @@ -228,7 +268,7 @@ illegal_chars: const QModelIndex mapped_index = proxy_model->mapFromSource(created_object); ui->listView->setCurrentIndex(mapped_index); } else { - std::filesystem::remove_all(dstPath); + QDir(dstPath).removeRecursively(); QMessageBox::critical(this, tr("Clone"), tr("Failed to clone VM for an unknown reason."), QMessageBox::Ok); return; }