From e792698480bd0f488da4376df33bf6c922bc0cd2 Mon Sep 17 00:00:00 2001 From: Brett Date: Tue, 20 Aug 2024 15:20:24 -0500 Subject: [PATCH] Merge pull request #114 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Better exception handling / error reporting for add job screen * Don't supress exceptions for potentially long running functions in bl… * Increase Blender pack_project_file timeout to 120s --- src/engines/blender/blender_engine.py | 27 ++++-- src/ui/add_job.py | 118 ++++++++++++++------------ 2 files changed, 83 insertions(+), 62 deletions(-) diff --git a/src/engines/blender/blender_engine.py b/src/engines/blender/blender_engine.py index c9e9c9f..568bd1a 100644 --- a/src/engines/blender/blender_engine.py +++ b/src/engines/blender/blender_engine.py @@ -55,7 +55,9 @@ class Blender(BaseRenderEngine): return subprocess.run([self.renderer_path(), '-b', project_path, '--python-expr', python_expression], capture_output=True, timeout=timeout, creationflags=_creationflags) except Exception as e: - logger.error(f"Error running python expression in blender: {e}") + err_msg = f"Error running python expression in blender: {e}" + logger.error(err_msg) + raise ChildProcessError(err_msg) else: raise FileNotFoundError(f'Project file not found: {project_path}') @@ -70,9 +72,16 @@ class Blender(BaseRenderEngine): command = [self.renderer_path(), '-b', '--python', script_path] if project_path: command.insert(2, project_path) - return subprocess.run(command, capture_output=True, timeout=timeout, creationflags=_creationflags) + result = subprocess.run(command, capture_output=True, timeout=timeout, creationflags=_creationflags) + return result + except subprocess.TimeoutExpired: + err_msg = f"Timed out after {timeout}s while running python script in blender: {script_path}" + logger.error(err_msg) + raise TimeoutError(err_msg) except Exception as e: - logger.exception(f"Error running python script in blender: {e}") + err_msg = f"Error running python script in blender: {e}" + logger.error(err_msg) + raise ChildProcessError(err_msg) def get_project_info(self, project_path, timeout=10): scene_info = {} @@ -89,10 +98,12 @@ class Blender(BaseRenderEngine): elif line.startswith('Error'): logger.error(f"get_scene_info error: {line.strip()}") except Exception as e: - logger.error(f'Error getting file details for .blend file: {e}') + msg = f'Error getting file details for .blend file: {e}' + logger.error(msg) + raise ChildProcessError(msg) return scene_info - def pack_project_file(self, project_path, timeout=30): + def pack_project_file(self, project_path, timeout=None): # Credit to L0Lock for pack script - https://blender.stackexchange.com/a/243935 try: logger.info(f"Starting to pack Blender file: {project_path}") @@ -115,7 +126,9 @@ class Blender(BaseRenderEngine): logger.info(f'Blender file packed successfully to {new_path}') return new_path except Exception as e: - logger.error(f'Error packing .blend file: {e}') + msg = f'Error packing .blend file: {e}' + logger.error(msg) + raise ChildProcessError(msg) return None def get_arguments(self): @@ -172,7 +185,7 @@ class Blender(BaseRenderEngine): return render_engines def perform_presubmission_tasks(self, project_path): - packed_path = self.pack_project_file(project_path, timeout=30) + packed_path = self.pack_project_file(project_path, timeout=120) return packed_path diff --git a/src/ui/add_job.py b/src/ui/add_job.py index 732ed7c..ea9eb23 100644 --- a/src/ui/add_job.py +++ b/src/ui/add_job.py @@ -377,7 +377,7 @@ class NewRenderJobForm(QWidget): self.cameras_group.setHidden(True) self.submit_button.setEnabled(enabled) - def after_job_submission(self, result): + def after_job_submission(self, error_string): # UI cleanup self.submit_progress.setMaximum(0) @@ -389,7 +389,7 @@ class NewRenderJobForm(QWidget): self.toggle_renderer_enablement(True) self.msg_box = QMessageBox() - if result.ok: + if not error_string: self.msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) self.msg_box.setIcon(QMessageBox.Icon.Information) self.msg_box.setText("Job successfully submitted to server. Submit another?") @@ -400,7 +400,7 @@ class NewRenderJobForm(QWidget): else: self.msg_box.setStandardButtons(QMessageBox.StandardButton.Ok) self.msg_box.setIcon(QMessageBox.Icon.Critical) - self.msg_box.setText(result.text or "Unknown error") + self.msg_box.setText(error_string) self.msg_box.setWindowTitle("Error") self.msg_box.exec() @@ -431,7 +431,7 @@ class NewRenderJobForm(QWidget): class SubmitWorker(QThread): """Worker class called to submit all the jobs to the server and update the UI accordingly""" - message_signal = pyqtSignal(Response) + message_signal = pyqtSignal(str) update_ui_signal = pyqtSignal(str, str) def __init__(self, window): @@ -447,61 +447,69 @@ class SubmitWorker(QThread): self.update_ui_signal.emit(hostname, percent) return callback - hostname = self.window.server_input.currentText() - job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(), - 'renderer': self.window.renderer_type.currentText().lower(), - 'engine_version': self.window.renderer_version_combo.currentText(), - 'args': {'raw': self.window.raw_args.text(), - 'export_format': self.window.file_format_combo.currentText()}, - 'output_path': self.window.render_name_input.text(), - 'start_frame': self.window.start_frame_input.value(), - 'end_frame': self.window.end_frame_input.value(), - 'priority': self.window.priority_input.currentIndex() + 1, - 'notes': self.window.notes_input.toPlainText(), - 'enable_split_jobs': self.window.enable_splitjobs.isChecked(), - 'split_jobs_same_os': self.window.splitjobs_same_os.isChecked(), - 'name': self.window.render_name_input.text()} + try: + hostname = self.window.server_input.currentText() + job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(), + 'renderer': self.window.renderer_type.currentText().lower(), + 'engine_version': self.window.renderer_version_combo.currentText(), + 'args': {'raw': self.window.raw_args.text(), + 'export_format': self.window.file_format_combo.currentText()}, + 'output_path': self.window.render_name_input.text(), + 'start_frame': self.window.start_frame_input.value(), + 'end_frame': self.window.end_frame_input.value(), + 'priority': self.window.priority_input.currentIndex() + 1, + 'notes': self.window.notes_input.toPlainText(), + 'enable_split_jobs': self.window.enable_splitjobs.isChecked(), + 'split_jobs_same_os': self.window.splitjobs_same_os.isChecked(), + 'name': self.window.render_name_input.text()} - # get the dynamic args - for i in range(self.window.renderer_options_layout.count()): - item = self.window.renderer_options_layout.itemAt(i) - layout = item.layout() # get the layout - for x in range(layout.count()): - z = layout.itemAt(x) - widget = z.widget() - if isinstance(widget, QComboBox): - job_json['args'][self.window.current_engine_options[i]['name']] = widget.currentText() - elif isinstance(widget, QLineEdit): - job_json['args'][self.window.current_engine_options[i]['name']] = widget.text() + # get the dynamic args + for i in range(self.window.renderer_options_layout.count()): + item = self.window.renderer_options_layout.itemAt(i) + layout = item.layout() # get the layout + for x in range(layout.count()): + z = layout.itemAt(x) + widget = z.widget() + if isinstance(widget, QComboBox): + job_json['args'][self.window.current_engine_options[i]['name']] = widget.currentText() + elif isinstance(widget, QLineEdit): + job_json['args'][self.window.current_engine_options[i]['name']] = widget.text() - # determine if any cameras are checked - selected_cameras = [] - if self.window.cameras_list.count() and not self.window.cameras_group.isHidden(): - for index in range(self.window.cameras_list.count()): - item = self.window.cameras_list.item(index) - if item.checkState() == Qt.CheckState.Checked: - selected_cameras.append(item.text().rsplit('-', 1)[0].strip()) # cleanup to just camera name + # determine if any cameras are checked + selected_cameras = [] + if self.window.cameras_list.count() and not self.window.cameras_group.isHidden(): + for index in range(self.window.cameras_list.count()): + item = self.window.cameras_list.item(index) + if item.checkState() == Qt.CheckState.Checked: + selected_cameras.append(item.text().rsplit('-', 1)[0].strip()) # cleanup to just camera name - # process cameras into nested format - input_path = self.window.scene_file_input.text() - if selected_cameras: - job_list = [] - for cam in selected_cameras: - job_copy = copy.deepcopy(job_json) - job_copy['args']['camera'] = cam - job_copy['name'] = job_copy['name'].replace(' ', '-') + "_" + cam.replace(' ', '') - job_copy['output_path'] = job_copy['name'] - job_list.append(job_copy) - else: - job_list = [job_json] + # process cameras into nested format + input_path = self.window.scene_file_input.text() + if selected_cameras: + job_list = [] + for cam in selected_cameras: + job_copy = copy.deepcopy(job_json) + job_copy['args']['camera'] = cam + job_copy['name'] = job_copy['name'].replace(' ', '-') + "_" + cam.replace(' ', '') + job_copy['output_path'] = job_copy['name'] + job_list.append(job_copy) + else: + job_list = [job_json] - # presubmission tasks - engine = EngineManager.engine_with_name(self.window.renderer_type.currentText().lower()) - input_path = engine().perform_presubmission_tasks(input_path) - # submit - result = self.window.server_proxy.post_job_to_server(file_path=input_path, job_list=job_list, - callback=create_callback) - self.message_signal.emit(result) + # presubmission tasks + engine = EngineManager.engine_with_name(self.window.renderer_type.currentText().lower()) + input_path = engine().perform_presubmission_tasks(input_path) + # submit + err_msg = "" + result = self.window.server_proxy.post_job_to_server(file_path=input_path, job_list=job_list, + callback=create_callback) + if not (result and result.ok): + err_msg = "Error posting job to server." + + self.message_signal.emit(err_msg) + + except Exception as e: + self.message_signal.emit(str(e)) class GetProjectInfoWorker(QThread):