STAR-322: Improving logging and cluster connection

This commit is contained in:
Daniel Schiavini 2018-12-17 11:28:16 +01:00
parent 9086105204
commit 9f4b7bd703
6 changed files with 51 additions and 35 deletions

View file

@ -52,7 +52,8 @@ class AuthorizationService:
if not self._user_profile: if not self._user_profile:
# If no user profile was stored locally, we try to get it from JWT. # If no user profile was stored locally, we try to get it from JWT.
self._user_profile = self._parseJWT() self._user_profile = self._parseJWT()
if not self._user_profile:
if not self._user_profile and self._auth_data:
# If there is still no user profile from the JWT, we have to log in again. # If there is still no user profile from the JWT, we have to log in again.
Logger.log("w", "The user profile could not be loaded. The user must log in again!") Logger.log("w", "The user profile could not be loaded. The user must log in again!")
self.deleteAuthData() self.deleteAuthData()

View file

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json import json
from json import JSONDecodeError from json import JSONDecodeError
from time import time
from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
@ -38,6 +39,8 @@ class CloudApiClient:
self._account = account self._account = account
self._on_error = on_error self._on_error = on_error
self._upload = None # type: Optional[MeshUploader] self._upload = None # type: Optional[MeshUploader]
# in order to avoid garbage collection we keep the callbacks in this list.
self._anti_gc_callbacks = [] # type: List[Callable[[QNetworkReply], None]]
## Gets the account used for the API. ## Gets the account used for the API.
@property @property
@ -49,8 +52,7 @@ class CloudApiClient:
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None: def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None:
url = "{}/clusters".format(self.CLUSTER_API_ROOT) url = "{}/clusters".format(self.CLUSTER_API_ROOT)
reply = self._manager.get(self._createEmptyRequest(url)) reply = self._manager.get(self._createEmptyRequest(url))
callback = self._wrapCallback(reply, on_finished, CloudClusterResponse) self._addCallbacks(reply, on_finished, CloudClusterResponse)
reply.finished.connect(callback)
## Retrieves the status of the given cluster. ## Retrieves the status of the given cluster.
# \param cluster_id: The ID of the cluster. # \param cluster_id: The ID of the cluster.
@ -58,8 +60,7 @@ class CloudApiClient:
def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None: def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None:
url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id) url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id)
reply = self._manager.get(self._createEmptyRequest(url)) reply = self._manager.get(self._createEmptyRequest(url))
callback = self._wrapCallback(reply, on_finished, CloudClusterStatus) self._addCallbacks(reply, on_finished, CloudClusterStatus)
reply.finished.connect(callback)
## Requests the cloud to register the upload of a print job mesh. ## Requests the cloud to register the upload of a print job mesh.
# \param request: The request object. # \param request: The request object.
@ -69,8 +70,7 @@ class CloudApiClient:
url = "{}/jobs/upload".format(self.CURA_API_ROOT) url = "{}/jobs/upload".format(self.CURA_API_ROOT)
body = json.dumps({"data": request.toDict()}) body = json.dumps({"data": request.toDict()})
reply = self._manager.put(self._createEmptyRequest(url), body.encode()) reply = self._manager.put(self._createEmptyRequest(url), body.encode())
callback = self._wrapCallback(reply, on_finished, CloudPrintJobResponse) self._addCallbacks(reply, on_finished, CloudPrintJobResponse)
reply.finished.connect(callback)
## Requests the cloud to register the upload of a print job mesh. ## Requests the cloud to register the upload of a print job mesh.
# \param upload_response: The object received after requesting an upload with `self.requestUpload`. # \param upload_response: The object received after requesting an upload with `self.requestUpload`.
@ -90,8 +90,7 @@ class CloudApiClient:
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None: def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None:
url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id) url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id)
reply = self._manager.post(self._createEmptyRequest(url), b"") reply = self._manager.post(self._createEmptyRequest(url), b"")
callback = self._wrapCallback(reply, on_finished, CloudPrintResponse) self._addCallbacks(reply, on_finished, CloudPrintResponse)
reply.finished.connect(callback)
## We override _createEmptyRequest in order to add the user credentials. ## We override _createEmptyRequest in order to add the user credentials.
# \param url: The URL to request # \param url: The URL to request
@ -116,9 +115,10 @@ class CloudApiClient:
Logger.log("i", "Received a reply %s from %s with %s", status_code, reply.url().toString(), response) Logger.log("i", "Received a reply %s from %s with %s", status_code, reply.url().toString(), response)
return status_code, json.loads(response) return status_code, json.loads(response)
except (UnicodeDecodeError, JSONDecodeError, ValueError) as err: except (UnicodeDecodeError, JSONDecodeError, ValueError) as err:
error = {"code": type(err).__name__, "title": str(err), "http_code": str(status_code)} error = CloudErrorObject(code=type(err).__name__, title=str(err), http_code=str(status_code),
id=str(time()), http_status="500")
Logger.logException("e", "Could not parse the stardust response: %s", error) Logger.logException("e", "Could not parse the stardust response: %s", error)
return status_code, {"errors": [error]} return status_code, {"errors": [error.toDict()]}
## The generic type variable used to document the methods below. ## The generic type variable used to document the methods below.
Model = TypeVar("Model", bound=BaseModel) Model = TypeVar("Model", bound=BaseModel)
@ -143,12 +143,15 @@ class CloudApiClient:
# \param on_finished: The callback in case the response is successful. # \param on_finished: The callback in case the response is successful.
# \param model: The type of the model to convert the response to. It may either be a single record or a list. # \param model: The type of the model to convert the response to. It may either be a single record or a list.
# \return: A function that can be passed to the # \return: A function that can be passed to the
def _wrapCallback(self, def _addCallbacks(self,
reply: QNetworkReply, reply: QNetworkReply,
on_finished: Callable[[Union[Model, List[Model]]], Any], on_finished: Callable[[Union[Model, List[Model]]], Any],
model: Type[Model], model: Type[Model],
) -> Callable[[QNetworkReply], None]: ) -> None:
def parse() -> None: def parse() -> None:
status_code, response = self._parseReply(reply) status_code, response = self._parseReply(reply)
self._anti_gc_callbacks.remove(parse)
return self._parseModels(response, on_finished, model) return self._parseModels(response, on_finished, model)
return parse
self._anti_gc_callbacks.append(parse)
reply.finished.connect(parse)

View file

@ -18,6 +18,7 @@ from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from plugins.UM3NetworkPrinting.src.Cloud.CloudOutputController import CloudOutputController from plugins.UM3NetworkPrinting.src.Cloud.CloudOutputController import CloudOutputController
from src.Cloud.Models.CloudClusterResponse import CloudClusterResponse
from ..MeshFormatHandler import MeshFormatHandler from ..MeshFormatHandler import MeshFormatHandler
from ..UM3PrintJobOutputModel import UM3PrintJobOutputModel from ..UM3PrintJobOutputModel import UM3PrintJobOutputModel
from .CloudProgressMessage import CloudProgressMessage from .CloudProgressMessage import CloudProgressMessage
@ -84,18 +85,15 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
# \param api_client: The client that will run the API calls # \param api_client: The client that will run the API calls
# \param device_id: The ID of the device (i.e. the cluster_id for the cloud API) # \param device_id: The ID of the device (i.e. the cluster_id for the cloud API)
# \param parent: The optional parent of this output device. # \param parent: The optional parent of this output device.
def __init__(self, api_client: CloudApiClient, device_id: str, host_name: str, parent: QObject = None) -> None: def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None:
super().__init__(device_id = device_id, address = "", properties = {}, parent = parent) super().__init__(device_id = cluster.cluster_id, address = "", properties = {}, parent = parent)
self._api = api_client self._api = api_client
self._host_name = host_name self._cluster = cluster
self._setInterfaceElements() self._setInterfaceElements()
self._device_id = device_id
self._account = api_client.account self._account = api_client.account
CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
# We use the Cura Connect monitor tab to get most functionality right away. # We use the Cura Connect monitor tab to get most functionality right away.
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"../../resources/qml/MonitorStage.qml") "../../resources/qml/MonitorStage.qml")
@ -124,7 +122,14 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
self._mesh = None # type: Optional[bytes] self._mesh = None # type: Optional[bytes]
self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse] self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
def connect(self) -> None:
super().connect()
Logger.log("i", "Connected to cluster %s", self.key)
CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
def disconnect(self) -> None: def disconnect(self) -> None:
super().disconnect()
Logger.log("i", "Disconnected to cluster %s", self.key)
CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange) CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange)
def _onBackendStateChange(self, _: BackendState) -> None: def _onBackendStateChange(self, _: BackendState) -> None:
@ -133,19 +138,19 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
## Gets the host name of this device ## Gets the host name of this device
@property @property
def host_name(self) -> str: def clusterData(self) -> CloudClusterResponse:
return self._host_name return self._cluster
## Updates the host name of the output device ## Updates the host name of the output device
@host_name.setter @clusterData.setter
def host_name(self, value: str) -> None: def clusterData(self, value: CloudClusterResponse) -> None:
self._host_name = value self._cluster = value
## Checks whether the given network key is found in the cloud's host name ## Checks whether the given network key is found in the cloud's host name
def matchesNetworkKey(self, network_key: str) -> bool: def matchesNetworkKey(self, network_key: str) -> bool:
# A network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local." # A network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
# the host name should then be "ultimakersystem-aabbccdd0011" # the host name should then be "ultimakersystem-aabbccdd0011"
return network_key.startswith(self._host_name) return network_key.startswith(self.clusterData.host_name)
## Set all the interface elements and texts for this output device. ## Set all the interface elements and texts for this output device.
def _setInterfaceElements(self) -> None: def _setInterfaceElements(self) -> None:
@ -170,7 +175,7 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
if self._uploaded_print_job: if self._uploaded_print_job:
# the mesh didn't change, let's not upload it again # the mesh didn't change, let's not upload it again
self._api.requestPrint(self._device_id, self._uploaded_print_job.job_id, self._onPrintRequested) self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintRequested)
return return
# Indicate we have started sending a job. # Indicate we have started sending a job.
@ -194,12 +199,15 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
## Called when the network data should be updated. ## Called when the network data should be updated.
def _update(self) -> None: def _update(self) -> None:
super()._update() super()._update()
if self._last_response_time and time() - self._last_response_time < self.CHECK_CLUSTER_INTERVAL: if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL:
Logger.log("i", "Not updating: %s - %s < %s", time(), self._last_request_time, self.CHECK_CLUSTER_INTERVAL)
return # avoid calling the cloud too often return # avoid calling the cloud too often
Logger.log("i", "Updating: %s - %s >= %s", time(), self._last_request_time, self.CHECK_CLUSTER_INTERVAL)
if self._account.isLoggedIn: if self._account.isLoggedIn:
self.setAuthenticationState(AuthState.Authenticated) self.setAuthenticationState(AuthState.Authenticated)
self._api.getClusterStatus(self._device_id, self._onStatusCallFinished) self._last_request_time = time()
self._api.getClusterStatus(self.key, self._onStatusCallFinished)
else: else:
self.setAuthenticationState(AuthState.NotAuthenticated) self.setAuthenticationState(AuthState.NotAuthenticated)
@ -315,7 +323,7 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
## Requests the print to be sent to the printer when we finished uploading the mesh. ## Requests the print to be sent to the printer when we finished uploading the mesh.
def _onPrintJobUploaded(self) -> None: def _onPrintJobUploaded(self) -> None:
self._progress.update(100) self._progress.update(100)
self._api.requestPrint(self._device_id, self._uploaded_print_job.job_id, self._onPrintRequested) self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintRequested)
## Displays the given message if uploading the mesh has failed ## Displays the given message if uploading the mesh has failed
# \param message: The message to display. # \param message: The message to display.

View file

@ -73,7 +73,8 @@ class CloudOutputDeviceManager:
removed_devices, added_clusters, updates = findChanges(self._remote_clusters, online_clusters) removed_devices, added_clusters, updates = findChanges(self._remote_clusters, online_clusters)
Logger.log("i", "Parsed remote clusters to %s", online_clusters) Logger.log("i", "Parsed remote clusters to %s", [cluster.toDict() for cluster in online_clusters.values()])
Logger.log("i", "Removed: %s, added: %s, updates: %s", len(removed_devices), len(added_clusters), len(updates))
# Remove output devices that are gone # Remove output devices that are gone
for removed_cluster in removed_devices: for removed_cluster in removed_devices:
@ -86,12 +87,12 @@ class CloudOutputDeviceManager:
# Add an output device for each new remote cluster. # Add an output device for each new remote cluster.
# We only add when is_online as we don't want the option in the drop down if the cluster is not online. # We only add when is_online as we don't want the option in the drop down if the cluster is not online.
for added_cluster in added_clusters: for added_cluster in added_clusters:
device = CloudOutputDevice(self._api, added_cluster.cluster_id, added_cluster.host_name) device = CloudOutputDevice(self._api, added_cluster)
self._output_device_manager.addOutputDevice(device) self._output_device_manager.addOutputDevice(device)
self._remote_clusters[added_cluster.cluster_id] = device self._remote_clusters[added_cluster.cluster_id] = device
for device, cluster in updates: for device, cluster in updates:
device.host_name = cluster.host_name device.clusterData = cluster
self._connectToActiveMachine() self._connectToActiveMachine()
@ -99,6 +100,7 @@ class CloudOutputDeviceManager:
def _connectToActiveMachine(self) -> None: def _connectToActiveMachine(self) -> None:
active_machine = CuraApplication.getInstance().getGlobalContainerStack() active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if not active_machine: if not active_machine:
Logger.log("i", "no active machine")
return return
# Check if the stored cluster_id for the active machine is in our list of remote clusters. # Check if the stored cluster_id for the active machine is in our list of remote clusters.
@ -107,6 +109,7 @@ class CloudOutputDeviceManager:
device = self._remote_clusters[stored_cluster_id] device = self._remote_clusters[stored_cluster_id]
if not device.isConnected(): if not device.isConnected():
device.connect() device.connect()
Logger.log("i", "Device connected by metadata %s", stored_cluster_id)
else: else:
self._connectByNetworkKey(active_machine) self._connectByNetworkKey(active_machine)
@ -122,6 +125,8 @@ class CloudOutputDeviceManager:
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
device.connect() device.connect()
Logger.log("i", "Found cluster %s with network key %s", device, local_network_key)
## Handles an API error received from the cloud. ## Handles an API error received from the cloud.
# \param errors: The errors received # \param errors: The errors received
def _onApiError(self, errors: List[CloudErrorObject]) -> None: def _onApiError(self, errors: List[CloudErrorObject]) -> None:

View file

@ -390,10 +390,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
## Called when the connection to the cluster changes. ## Called when the connection to the cluster changes.
def connect(self) -> None: def connect(self) -> None:
pass
# TODO: uncomment this once cloud implementation works for testing # TODO: uncomment this once cloud implementation works for testing
# super().connect() # super().connect()
# self.sendMaterialProfiles() # self.sendMaterialProfiles()
pass
def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None: def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None:
reply_url = reply.url().toString() reply_url = reply.url().toString()

View file

@ -18,4 +18,3 @@ class ClusterUM3PrinterOutputController(PrinterOutputController):
def setJobState(self, job: "PrintJobOutputModel", state: str): def setJobState(self, job: "PrintJobOutputModel", state: str):
data = "{\"action\": \"%s\"}" % state data = "{\"action\": \"%s\"}" % state
self._output_device.put("print_jobs/%s/action" % job.key, data, on_finished=None) self._output_device.put("print_jobs/%s/action" % job.key, data, on_finished=None)