diff --git a/Code/Tools/ProjectManager/Source/DownloadController.cpp b/Code/Tools/ProjectManager/Source/DownloadController.cpp index fa3fdb10d1..3ee800bba8 100644 --- a/Code/Tools/ProjectManager/Source/DownloadController.cpp +++ b/Code/Tools/ProjectManager/Source/DownloadController.cpp @@ -8,10 +8,15 @@ #include #include +#include + +#include #include + + namespace O3DE::ProjectManager { DownloadController::DownloadController(QWidget* parent) @@ -46,6 +51,24 @@ namespace O3DE::ProjectManager } } + void DownloadController::CancelGemDownload(const QString& gemName) + { + auto findResult = AZStd::find(m_gemNames.begin(), m_gemNames.end(), gemName); + + if (findResult != m_gemNames.end()) + { + if (findResult == m_gemNames.begin()) + { + // HandleResults will remove the gem upon cancelling + PythonBindingsInterface::Get()->CancelDownload(); + } + else + { + m_gemNames.erase(findResult); + } + } + } + void DownloadController::UpdateUIProgress(int progress) { m_lastProgress = progress; @@ -75,10 +98,4 @@ namespace O3DE::ProjectManager m_workerThread.wait(); } } - - void DownloadController::HandleCancel() - { - m_workerThread.quit(); - emit Done(false); - } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/DownloadController.h b/Code/Tools/ProjectManager/Source/DownloadController.h index 608d9b1a2b..11ceaacddb 100644 --- a/Code/Tools/ProjectManager/Source/DownloadController.h +++ b/Code/Tools/ProjectManager/Source/DownloadController.h @@ -27,7 +27,8 @@ namespace O3DE::ProjectManager explicit DownloadController(QWidget* parent = nullptr); ~DownloadController(); - void AddGemDownload(const QString& m_gemName); + void AddGemDownload(const QString& gemName); + void CancelGemDownload(const QString& gemName); bool IsDownloadQueueEmpty() { @@ -54,7 +55,6 @@ namespace O3DE::ProjectManager public slots: void UpdateUIProgress(int progress); void HandleResults(const QString& result); - void HandleCancel(); signals: void StartGemDownload(const QString& gemName); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp index 79ff7624b7..875d5ada8c 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp @@ -154,6 +154,11 @@ namespace O3DE::ProjectManager update(); } + void CartOverlayWidget::OnCancelDownloadActivated(const QString& gemName) + { + m_downloadController->CancelGemDownload(gemName); + } + void CartOverlayWidget::CreateDownloadSection() { QWidget* widget = new QWidget(); @@ -188,6 +193,8 @@ namespace O3DE::ProjectManager downloadingItemLayout->setAlignment(Qt::AlignTop); downloadingItemWidget->setLayout(downloadingItemLayout); + m_downloadController->AddGemDownload("TestGem"); + auto update = [=](int downloadProgress) { if (m_downloadController->IsDownloadQueueEmpty()) @@ -234,7 +241,9 @@ namespace O3DE::ProjectManager nameProgressLayout->addWidget(progress); QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); nameProgressLayout->addSpacerItem(spacer); - QLabel* cancelText = new QLabel(tr("Cancel")); + QLabel* cancelText = new QLabel(QString("Cancel").arg(downloadQueue[downloadingGemNumber])); + cancelText->setTextInteractionFlags(Qt::LinksAccessibleByMouse); + connect(cancelText, &QLabel::linkActivated, this, &CartOverlayWidget::OnCancelDownloadActivated); nameProgressLayout->addWidget(cancelText); downloadingItemLayout->addLayout(nameProgressLayout); QProgressBar* downloadProgessBar = new QProgressBar(); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h index 8e0eaa13ba..9f1e592d6e 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h @@ -40,6 +40,7 @@ namespace O3DE::ProjectManager using GetTagIndicesCallback = AZStd::function()>; void CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices); void CreateDownloadSection(); + void OnCancelDownloadActivated(const QString& link); QVBoxLayout* m_layout = nullptr; GemModel* m_gemModel = nullptr; diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index bc8773b0c8..8b716666de 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -223,6 +223,50 @@ namespace RedirectOutput } } // namespace RedirectOutput +namespace O3DEProjectManagerPy +{ + using DownloadProgressFunc = AZStd::function; + DownloadProgressFunc currentProgressCallback; + bool requestCancelDownload = false; + + static PyObject* CLICancelDownload(PyObject* /*self*/, PyObject* /*args*/) + { + if (requestCancelDownload) + { + return Py_True; + } + else + { + return Py_False; + } + } + + static PyObject* CLIDownloadProgress(PyObject* /*self*/, PyObject* args) + { + int progress_percentage = 0; + if (!PyArg_ParseTuple(args, "i", &progress_percentage)) + return NULL; + if (currentProgressCallback) + { + currentProgressCallback(progress_percentage); + } + + Py_RETURN_NONE; + } + + static PyMethodDef O3DEPMPyMethods[] = { { "download_progress", CLIDownloadProgress, METH_VARARGS, "Used to call back to the UI to inform of download progress." }, + { "request_cancel_download", CLICancelDownload, METH_NOARGS, "Returns that the UI is requesting that the current download be cancelled." }, + { NULL, NULL, 0, NULL } }; + + static PyModuleDef O3DEPMPyModule = { PyModuleDef_HEAD_INIT, "o3de_projectmanager", NULL, -1, O3DEPMPyMethods, NULL, NULL, NULL, NULL }; + + static PyObject* PyInit_O3DEPMPy(void) + { + return PyModule_Create(&O3DEPMPyModule); + } +} // namespace O3DEProjectManagerPy + + namespace O3DE::ProjectManager { PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath) @@ -270,6 +314,7 @@ namespace O3DE::ProjectManager AZ_TracePrintf("python", "Py_GetProgramFullPath=%ls \n", Py_GetProgramFullPath()); PyImport_AppendInittab("azlmbr_redirect", RedirectOutput::PyInit_RedirectOutput); + PyImport_AppendInittab("o3de_projectmanager", &O3DEProjectManagerPy::PyInit_O3DEPMPy); try { @@ -1080,6 +1125,8 @@ namespace O3DE::ProjectManager AZ::Outcome PythonBindings::DownloadGem(const QString& gemName, std::function gemProgressCallback) { bool downloadSucceeded = false; + O3DEProjectManagerPy::currentProgressCallback = gemProgressCallback; + O3DEProjectManagerPy::requestCancelDownload = false; auto result = ExecuteWithLockErrorHandling( [&] { @@ -1091,6 +1138,8 @@ namespace O3DE::ProjectManager downloadSucceeded = (downloadResult.cast() == 0); }); + O3DEProjectManagerPy::currentProgressCallback = nullptr; + if (!result.IsSuccess()) { return result; @@ -1102,4 +1151,9 @@ namespace O3DE::ProjectManager return AZ::Success(); } + + void PythonBindings::CancelDownload() + { + O3DEProjectManagerPy::requestCancelDownload = true; + } } diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.h b/Code/Tools/ProjectManager/Source/PythonBindings.h index 638ce6b1d4..386268b5b2 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.h +++ b/Code/Tools/ProjectManager/Source/PythonBindings.h @@ -62,6 +62,7 @@ namespace O3DE::ProjectManager bool RemoveGemRepo(const QString& repoUri) override; AZ::Outcome, AZStd::string> GetAllGemRepoInfos() override; AZ::Outcome DownloadGem(const QString& gemName, std::function gemProgressCallback) override; + void CancelDownload() override; private: AZ_DISABLE_COPY_MOVE(PythonBindings); diff --git a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h index 19442540a4..928e9a8d48 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h +++ b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h @@ -188,7 +188,18 @@ namespace O3DE::ProjectManager */ virtual AZ::Outcome, AZStd::string> GetAllGemRepoInfos() = 0; + /** + * Downloads and registers a Gem. + * @param gemName the name of the Gem to download + * @param gemProgressCallback a callback function that is called with an int percentage download value + * @return an outcome with a string error message on failure. + */ virtual AZ::Outcome DownloadGem(const QString& gemName, std::function gemProgressCallback) = 0; + + /** + * Cancels the current download. + */ + virtual void CancelDownload() = 0; }; using PythonBindingsInterface = AZ::Interface; diff --git a/scripts/o3de/o3de/utils.py b/scripts/o3de/o3de/utils.py index c5be21c60f..cce51bbd8f 100755 --- a/scripts/o3de/o3de/utils.py +++ b/scripts/o3de/o3de/utils.py @@ -10,15 +10,44 @@ This file contains utility functions """ import sys import uuid +import os import pathlib import shutil import urllib.request import logging import zipfile +try: + import o3de_projectmanager +except ImportError: + pass logger = logging.getLogger() logging.basicConfig() +COPY_BUFSIZE = 64 * 1024 + +def copyfileobj(fsrc, fdst, callback, length=0): + # This is functionally the same as the python shutil copyfileobj but + # allows for a callback to return the download progress in blocks and allows + # to early out to cancel the copy. + if not length: + length = COPY_BUFSIZE + + fsrc_read = fsrc.read + fdst_write = fdst.write + + copied = 0 + while True: + if o3de_projectmanager and o3de_projectmanager.request_cancel_download(): + return 1 + buf = fsrc_read(length) + if not buf: + break + fdst_write(buf) + copied += len(buf) + callback(copied) + return 0 + def validate_identifier(identifier: str) -> bool: """ Determine if the identifier supplied is valid. @@ -93,7 +122,6 @@ def backup_folder(folder: str or pathlib.Path) -> None: if backup_folder_name.is_dir(): renamed = True - def download_file(parsed_uri, download_path: pathlib.Path) -> int: """ :param parsed_uri: uniform resource identifier to zip file to download @@ -103,8 +131,18 @@ def download_file(parsed_uri, download_path: pathlib.Path) -> int: logger.warn(f'File already downloaded to {download_path}.') elif parsed_uri.scheme in ['http', 'https', 'ftp', 'ftps']: with urllib.request.urlopen(parsed_uri.geturl()) as s: + download_file_size = 0 + try: + download_file_size = s.headers['content-length'] + except KeyError: + pass + def download_progress(blocks): + if o3de_projectmanager and download_file_size: + o3de_projectmanager.download_progress(int(blocks/int(download_file_size) * 100)) with download_path.open('wb') as f: - shutil.copyfileobj(s, f) + download_cancelled = copyfileobj(s, f, download_progress) + if download_cancelled: + return 1 else: origin_file = pathlib.Path(parsed_uri.geturl()).resolve() if not origin_file.is_file():