Add the ability for Python to pass download progress to Project Manager and to cancel downloads.

Signed-off-by: AMZN-Phil <pconroy@amazon.com>
monroegm-disable-blank-issue-2
AMZN-Phil 4 years ago
parent 50068bcae7
commit e3cfcd4cc7

@ -8,10 +8,15 @@
#include <DownloadController.h> #include <DownloadController.h>
#include <DownloadWorker.h> #include <DownloadWorker.h>
#include <PythonBindings.h>
#include <AzCore/std/algorithm.h>
#include <QMessageBox> #include <QMessageBox>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
DownloadController::DownloadController(QWidget* parent) 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) void DownloadController::UpdateUIProgress(int progress)
{ {
m_lastProgress = progress; m_lastProgress = progress;
@ -75,10 +98,4 @@ namespace O3DE::ProjectManager
m_workerThread.wait(); m_workerThread.wait();
} }
} }
void DownloadController::HandleCancel()
{
m_workerThread.quit();
emit Done(false);
}
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -27,7 +27,8 @@ namespace O3DE::ProjectManager
explicit DownloadController(QWidget* parent = nullptr); explicit DownloadController(QWidget* parent = nullptr);
~DownloadController(); ~DownloadController();
void AddGemDownload(const QString& m_gemName); void AddGemDownload(const QString& gemName);
void CancelGemDownload(const QString& gemName);
bool IsDownloadQueueEmpty() bool IsDownloadQueueEmpty()
{ {
@ -54,7 +55,6 @@ namespace O3DE::ProjectManager
public slots: public slots:
void UpdateUIProgress(int progress); void UpdateUIProgress(int progress);
void HandleResults(const QString& result); void HandleResults(const QString& result);
void HandleCancel();
signals: signals:
void StartGemDownload(const QString& gemName); void StartGemDownload(const QString& gemName);

@ -154,6 +154,11 @@ namespace O3DE::ProjectManager
update(); update();
} }
void CartOverlayWidget::OnCancelDownloadActivated(const QString& gemName)
{
m_downloadController->CancelGemDownload(gemName);
}
void CartOverlayWidget::CreateDownloadSection() void CartOverlayWidget::CreateDownloadSection()
{ {
QWidget* widget = new QWidget(); QWidget* widget = new QWidget();
@ -188,6 +193,8 @@ namespace O3DE::ProjectManager
downloadingItemLayout->setAlignment(Qt::AlignTop); downloadingItemLayout->setAlignment(Qt::AlignTop);
downloadingItemWidget->setLayout(downloadingItemLayout); downloadingItemWidget->setLayout(downloadingItemLayout);
m_downloadController->AddGemDownload("TestGem");
auto update = [=](int downloadProgress) auto update = [=](int downloadProgress)
{ {
if (m_downloadController->IsDownloadQueueEmpty()) if (m_downloadController->IsDownloadQueueEmpty())
@ -234,7 +241,9 @@ namespace O3DE::ProjectManager
nameProgressLayout->addWidget(progress); nameProgressLayout->addWidget(progress);
QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
nameProgressLayout->addSpacerItem(spacer); nameProgressLayout->addSpacerItem(spacer);
QLabel* cancelText = new QLabel(tr("Cancel")); QLabel* cancelText = new QLabel(QString("<a href=\"%1\">Cancel</a>").arg(downloadQueue[downloadingGemNumber]));
cancelText->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
connect(cancelText, &QLabel::linkActivated, this, &CartOverlayWidget::OnCancelDownloadActivated);
nameProgressLayout->addWidget(cancelText); nameProgressLayout->addWidget(cancelText);
downloadingItemLayout->addLayout(nameProgressLayout); downloadingItemLayout->addLayout(nameProgressLayout);
QProgressBar* downloadProgessBar = new QProgressBar(); QProgressBar* downloadProgessBar = new QProgressBar();

@ -40,6 +40,7 @@ namespace O3DE::ProjectManager
using GetTagIndicesCallback = AZStd::function<QVector<QModelIndex>()>; using GetTagIndicesCallback = AZStd::function<QVector<QModelIndex>()>;
void CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices); void CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices);
void CreateDownloadSection(); void CreateDownloadSection();
void OnCancelDownloadActivated(const QString& link);
QVBoxLayout* m_layout = nullptr; QVBoxLayout* m_layout = nullptr;
GemModel* m_gemModel = nullptr; GemModel* m_gemModel = nullptr;

@ -223,6 +223,50 @@ namespace RedirectOutput
} }
} // namespace RedirectOutput } // namespace RedirectOutput
namespace O3DEProjectManagerPy
{
using DownloadProgressFunc = AZStd::function<void(int)>;
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 namespace O3DE::ProjectManager
{ {
PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath) PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath)
@ -270,6 +314,7 @@ namespace O3DE::ProjectManager
AZ_TracePrintf("python", "Py_GetProgramFullPath=%ls \n", Py_GetProgramFullPath()); AZ_TracePrintf("python", "Py_GetProgramFullPath=%ls \n", Py_GetProgramFullPath());
PyImport_AppendInittab("azlmbr_redirect", RedirectOutput::PyInit_RedirectOutput); PyImport_AppendInittab("azlmbr_redirect", RedirectOutput::PyInit_RedirectOutput);
PyImport_AppendInittab("o3de_projectmanager", &O3DEProjectManagerPy::PyInit_O3DEPMPy);
try try
{ {
@ -1080,6 +1125,8 @@ namespace O3DE::ProjectManager
AZ::Outcome<void, AZStd::string> PythonBindings::DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) AZ::Outcome<void, AZStd::string> PythonBindings::DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback)
{ {
bool downloadSucceeded = false; bool downloadSucceeded = false;
O3DEProjectManagerPy::currentProgressCallback = gemProgressCallback;
O3DEProjectManagerPy::requestCancelDownload = false;
auto result = ExecuteWithLockErrorHandling( auto result = ExecuteWithLockErrorHandling(
[&] [&]
{ {
@ -1091,6 +1138,8 @@ namespace O3DE::ProjectManager
downloadSucceeded = (downloadResult.cast<int>() == 0); downloadSucceeded = (downloadResult.cast<int>() == 0);
}); });
O3DEProjectManagerPy::currentProgressCallback = nullptr;
if (!result.IsSuccess()) if (!result.IsSuccess())
{ {
return result; return result;
@ -1102,4 +1151,9 @@ namespace O3DE::ProjectManager
return AZ::Success(); return AZ::Success();
} }
void PythonBindings::CancelDownload()
{
O3DEProjectManagerPy::requestCancelDownload = true;
}
} }

@ -62,6 +62,7 @@ namespace O3DE::ProjectManager
bool RemoveGemRepo(const QString& repoUri) override; bool RemoveGemRepo(const QString& repoUri) override;
AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override; AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override;
AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) override; AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) override;
void CancelDownload() override;
private: private:
AZ_DISABLE_COPY_MOVE(PythonBindings); AZ_DISABLE_COPY_MOVE(PythonBindings);

@ -188,7 +188,18 @@ namespace O3DE::ProjectManager
*/ */
virtual AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() = 0; virtual AZ::Outcome<QVector<GemRepoInfo>, 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<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) = 0; virtual AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) = 0;
/**
* Cancels the current download.
*/
virtual void CancelDownload() = 0;
}; };
using PythonBindingsInterface = AZ::Interface<IPythonBindings>; using PythonBindingsInterface = AZ::Interface<IPythonBindings>;

@ -10,15 +10,44 @@ This file contains utility functions
""" """
import sys import sys
import uuid import uuid
import os
import pathlib import pathlib
import shutil import shutil
import urllib.request import urllib.request
import logging import logging
import zipfile import zipfile
try:
import o3de_projectmanager
except ImportError:
pass
logger = logging.getLogger() logger = logging.getLogger()
logging.basicConfig() 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: def validate_identifier(identifier: str) -> bool:
""" """
Determine if the identifier supplied is valid. 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(): if backup_folder_name.is_dir():
renamed = True renamed = True
def download_file(parsed_uri, download_path: pathlib.Path) -> int: def download_file(parsed_uri, download_path: pathlib.Path) -> int:
""" """
:param parsed_uri: uniform resource identifier to zip file to download :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}.') logger.warn(f'File already downloaded to {download_path}.')
elif parsed_uri.scheme in ['http', 'https', 'ftp', 'ftps']: elif parsed_uri.scheme in ['http', 'https', 'ftp', 'ftps']:
with urllib.request.urlopen(parsed_uri.geturl()) as s: 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: with download_path.open('wb') as f:
shutil.copyfileobj(s, f) download_cancelled = copyfileobj(s, f, download_progress)
if download_cancelled:
return 1
else: else:
origin_file = pathlib.Path(parsed_uri.geturl()).resolve() origin_file = pathlib.Path(parsed_uri.geturl()).resolve()
if not origin_file.is_file(): if not origin_file.is_file():

Loading…
Cancel
Save