mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-03-05 18:14:38 -07:00
Add functionality to cancel print-job-upload in progress.
Sometimes this got stuck, and there was no way for users to get rid of (or even retry). CURA-11196
This commit is contained in:
parent
2495cfe464
commit
902f2b1ebe
5 changed files with 78 additions and 12 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2026 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
import urllib.parse
|
||||
|
|
@ -11,6 +11,7 @@ from PyQt6.QtCore import QUrl
|
|||
from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.TaskManagement.HttpRequestData import HttpRequestData
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
||||
from cura.API import Account
|
||||
|
|
@ -119,7 +120,7 @@ class CloudApiClient:
|
|||
timeout=self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
def requestUpload(self, request: CloudPrintJobUploadRequest,
|
||||
on_finished: Callable[[CloudPrintJobResponse], Any]) -> None:
|
||||
on_finished: Callable[[CloudPrintJobResponse], Any]) -> HttpRequestData:
|
||||
|
||||
"""Requests the cloud to register the upload of a print job mesh.
|
||||
|
||||
|
|
@ -130,14 +131,14 @@ class CloudApiClient:
|
|||
url = f"{self.CURA_API_ROOT}/jobs/upload"
|
||||
data = json.dumps({"data": request.toDict()}).encode()
|
||||
|
||||
self._http.put(url,
|
||||
return self._http.put(url,
|
||||
scope=self._scope,
|
||||
data=data,
|
||||
callback=self._parseCallback(on_finished, CloudPrintJobResponse),
|
||||
timeout=self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any],
|
||||
on_progress: Callable[[int], Any], on_error: Callable[[], Any]):
|
||||
on_progress: Callable[[int], Any], on_error: Callable[[], Any]) -> ToolPathUploader:
|
||||
"""Uploads a print job tool path to the cloud.
|
||||
|
||||
:param print_job: The object received after requesting an upload with `self.requestUpload`.
|
||||
|
|
@ -149,6 +150,7 @@ class CloudApiClient:
|
|||
|
||||
self._upload = ToolPathUploader(self._http, print_job, mesh, on_finished, on_progress, on_error)
|
||||
self._upload.start()
|
||||
return self._upload
|
||||
|
||||
# Requests a cluster to print the given print job.
|
||||
# \param cluster_id: The ID of the cluster.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2022 UltiMaker
|
||||
# Copyright (c) 2026 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from time import time
|
||||
|
|
@ -13,6 +13,7 @@ from UM import i18nCatalog
|
|||
from UM.FileHandler.FileHandler import FileHandler
|
||||
from UM.Logger import Logger
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.TaskManagement.HttpRequestData import HttpRequestData
|
||||
from UM.Version import Version
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
|
||||
|
|
@ -21,6 +22,7 @@ from cura.Scene.GCodeListDecorator import GCodeListDecorator
|
|||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
|
||||
from .CloudApiClient import CloudApiClient
|
||||
from .ToolPathUploader import ToolPathUploader
|
||||
from ..ExportFileJob import ExportFileJob
|
||||
from ..Messages.PrintJobAwaitingApprovalMessage import PrintJobPendingApprovalMessage
|
||||
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
|
||||
|
|
@ -117,6 +119,10 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
CuraApplication.getInstance().getBackend().backendDone.connect(self._resetPrintJob)
|
||||
CuraApplication.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
|
||||
|
||||
self._pre_uploader_handle: Optional[HttpRequestData] = None
|
||||
self._uploader_handle: Optional[ToolPathUploader] = None
|
||||
self._progress.actionTriggered.connect(self._onProgressMessageActionTriggered)
|
||||
|
||||
def connect(self) -> None:
|
||||
"""Connects this device."""
|
||||
|
||||
|
|
@ -139,11 +145,22 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
if node.getDecorator(GCodeListDecorator) or node.getDecorator(SliceableObjectDecorator):
|
||||
self._resetPrintJob()
|
||||
|
||||
def _cancelInProgressJobs(self):
|
||||
if self._pre_uploader_handle is not None:
|
||||
if self._pre_uploader_handle.reply is not None:
|
||||
self._pre_uploader_handle.reply.abort()
|
||||
self._pre_uploader_handle.setDone()
|
||||
self._pre_uploader_handle = None
|
||||
if self._uploader_handle is not None:
|
||||
self._uploader_handle.cancel()
|
||||
self._uploader_handle = None
|
||||
|
||||
def _resetPrintJob(self) -> None:
|
||||
"""Resets the print job that was uploaded to force a new upload, runs whenever slice finishes."""
|
||||
self._tool_path = None
|
||||
self._pre_upload_print_job = None
|
||||
self._uploaded_print_job = None
|
||||
self._cancelInProgressJobs()
|
||||
|
||||
def matchesNetworkKey(self, network_key: str) -> bool:
|
||||
"""Checks whether the given network key is found in the cloud's host name"""
|
||||
|
|
@ -240,7 +257,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
file_size=len(output),
|
||||
content_type=job.getMimeType(),
|
||||
)
|
||||
self._api.requestUpload(request, self._uploadPrintJob)
|
||||
self._pre_uploader_handle = self._api.requestUpload(request, self._uploadPrintJob)
|
||||
|
||||
def _uploadPrintJob(self, job_response: CloudPrintJobResponse) -> None:
|
||||
"""Uploads the mesh when the print job was registered with the cloud API.
|
||||
|
|
@ -250,9 +267,10 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
|
||||
if not self._tool_path:
|
||||
return self._onUploadError()
|
||||
self._pre_uploader_handle = None
|
||||
self._pre_upload_print_job = job_response # store the last uploaded job to prevent re-upload of the same file
|
||||
self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update,
|
||||
self._onUploadError)
|
||||
self._uploader_handle = self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded,
|
||||
self._progress.update, self._onUploadError)
|
||||
|
||||
def _onPrintJobUploaded(self) -> None:
|
||||
"""
|
||||
|
|
@ -267,6 +285,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
# error, which sets self._pre_uploaded_print_job to `None`.
|
||||
self._pre_upload_print_job = None
|
||||
self._uploaded_print_job = None
|
||||
self._pre_uploader_handle = None
|
||||
self._uploader_handle = None
|
||||
Logger.log("w", "Interference from another job uploaded at roughly the same time, not uploading print!")
|
||||
return # Prevent a crash.
|
||||
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted,
|
||||
|
|
@ -315,6 +335,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._progress.hide()
|
||||
self._pre_upload_print_job = None
|
||||
self._uploaded_print_job = None
|
||||
self._pre_uploader_handle = None
|
||||
self._uploader_handle = None
|
||||
self.writeError.emit()
|
||||
|
||||
def _onUploadError(self, message: str = None) -> None:
|
||||
|
|
@ -327,9 +349,16 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._progress.hide()
|
||||
self._pre_upload_print_job = None
|
||||
self._uploaded_print_job = None
|
||||
self._cancelInProgressJobs()
|
||||
PrintJobUploadErrorMessage(message).show()
|
||||
self.writeError.emit()
|
||||
|
||||
def _onProgressMessageActionTriggered(self, message: "Message", action: str):
|
||||
if action == "abort_upload":
|
||||
self._onUploadError(I18N_CATALOG.i18nc("@info:message", "The send-print-job process was aborted. Please try again."))
|
||||
else:
|
||||
Logger.warning(f"Unknown action {action} triggered on print-job upload progress message.")
|
||||
|
||||
@pyqtProperty(bool, notify=_cloudClusterPrintersChanged)
|
||||
def isMethod(self) -> bool:
|
||||
"""Whether the printer that this output device represents is a Method series printer."""
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
|
|||
from typing import Callable, Any, Tuple, cast, Dict, Optional
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.TaskManagement.HttpRequestData import HttpRequestData
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||
|
||||
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
|
||||
|
|
@ -44,6 +45,8 @@ class ToolPathUploader:
|
|||
self._retries = 0
|
||||
self._finished = False
|
||||
|
||||
self._uploader_handle: Optional[HttpRequestData] = None
|
||||
|
||||
@property
|
||||
def printJob(self):
|
||||
"""Returns the print job for which this object was created."""
|
||||
|
|
@ -63,9 +66,19 @@ class ToolPathUploader:
|
|||
"""Stops uploading the mesh, marking it as finished."""
|
||||
|
||||
Logger.log("i", "Finished uploading")
|
||||
self._uploader_handle = None
|
||||
self._finished = True # Signal to any ongoing retries that we should stop retrying.
|
||||
self._on_finished()
|
||||
|
||||
def cancel(self):
|
||||
"""Cancels the upload, marking it as finished."""
|
||||
if self._uploader_handle is not None:
|
||||
if self._uploader_handle.reply is not None:
|
||||
self._uploader_handle.reply.abort()
|
||||
self._uploader_handle.setDone()
|
||||
self.stop()
|
||||
self._on_error()
|
||||
|
||||
def _upload(self) -> None:
|
||||
"""
|
||||
Uploads the print job to the cloud printer.
|
||||
|
|
@ -74,7 +87,7 @@ class ToolPathUploader:
|
|||
raise ValueError("The upload is already finished")
|
||||
|
||||
Logger.log("i", "Uploading print to {upload_url}".format(upload_url = self._print_job.upload_url))
|
||||
self._http.put(
|
||||
self._uploader_handle = self._http.put(
|
||||
url = cast(str, self._print_job.upload_url),
|
||||
headers_dict = {"Content-Type": cast(str, self._print_job.content_type)},
|
||||
data = self._data,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2026 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from UM import i18nCatalog
|
||||
from UM.Message import Message
|
||||
|
|
@ -19,6 +19,12 @@ class PrintJobUploadProgressMessage(Message):
|
|||
dismissable = False,
|
||||
use_inactivity_timer = False
|
||||
)
|
||||
self.addAction(
|
||||
"abort_upload",
|
||||
I18N_CATALOG.i18nc("@action:button", "Abort"),
|
||||
"",
|
||||
I18N_CATALOG.i18nc("@action:label", "Abort upload"),
|
||||
)
|
||||
|
||||
def show(self):
|
||||
"""Shows the progress message."""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2025 UltiMaker
|
||||
# Copyright (c) 2026 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import os
|
||||
import platform
|
||||
|
|
@ -11,6 +11,7 @@ from PyQt6.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
|
|||
from PyQt6.QtNetwork import QNetworkReply
|
||||
|
||||
from UM.FileHandler.FileHandler import FileHandler
|
||||
from UM.TaskManagement.HttpRequestData import HttpRequestData
|
||||
from UM.Version import Version
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
|
|
@ -57,6 +58,9 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._setInterfaceElements()
|
||||
self._active_camera_url = QUrl() # type: QUrl
|
||||
|
||||
self._uploader_handle: Optional[QNetworkReply] = None
|
||||
self._progress.actionTriggered.connect(self._onProgressMessageActionTriggered)
|
||||
|
||||
def _setInterfaceElements(self) -> None:
|
||||
"""Set all the interface elements and texts for this output device."""
|
||||
|
||||
|
|
@ -145,6 +149,8 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
PrintJobUploadBlockedMessage().show()
|
||||
return
|
||||
|
||||
self._uploader_handle = None
|
||||
|
||||
self.writeStarted.emit(self)
|
||||
|
||||
# Export the scene to the correct file type.
|
||||
|
|
@ -207,7 +213,7 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
if unique_name is not None:
|
||||
parts.append(self._createFormPart("name=require_printer_name", bytes(unique_name, "utf-8"), "text/plain"))
|
||||
# FIXME: move form posting to API client
|
||||
self.postFormWithParts(
|
||||
self._uploader_handle = self.postFormWithParts(
|
||||
"/cluster-api/v1/print_jobs/",
|
||||
parts,
|
||||
on_finished=self._onPrintUploadCompleted,
|
||||
|
|
@ -228,6 +234,7 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
def _onPrintUploadCompleted(self, reply: QNetworkReply) -> None:
|
||||
"""Handler for when the print job was fully uploaded to the cluster."""
|
||||
|
||||
self._uploader_handle = None
|
||||
self._progress.hide()
|
||||
if reply.error() == QNetworkReply.NetworkError.NoError:
|
||||
PrintJobUploadSuccessMessage().show()
|
||||
|
|
@ -241,10 +248,19 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
:param message: The message to display.
|
||||
"""
|
||||
|
||||
self._uploader_handle = None
|
||||
self._progress.hide()
|
||||
PrintJobUploadErrorMessage(message).show()
|
||||
self.writeError.emit()
|
||||
|
||||
def _onProgressMessageActionTriggered(self, message: "Message", action: str):
|
||||
if action == "abort_upload":
|
||||
if self._uploader_handle is not None:
|
||||
self._uploader_handle.abort()
|
||||
self._onUploadError(I18N_CATALOG.i18nc("@info:message", "The send-print-job process was aborted. Please try again."))
|
||||
else:
|
||||
Logger.warning(f"Unknown action {action} triggered on print-job upload progress message.")
|
||||
|
||||
def _updatePrintJobPreviewImages(self):
|
||||
"""Download all the images from the cluster and load their data in the print job models."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue