From af0a2822d148459fad1348c229196621a0a69bc4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 14 Mar 2025 13:40:37 +1100 Subject: [PATCH] Call machine func (#9191) (#9298) * Force label printing to background worker * Refactor "check_reload" state of machine registry - In line with plugin registry - More work can be done here (i.e. session caching) * Better handling of call_plugin_function * Wrapper for calling machine function * Use AttributeError instead * Simplify function offloading * Check plugin registry hash when reloading machine registry * Cleanup * Fixes * Adjust unit test * Cleanup * Allow running in foreground if background worker not running * Simplify call structure --- src/backend/InvenTree/machine/machine_type.py | 10 ++- src/backend/InvenTree/machine/registry.py | 83 ++++++++++++++++++- .../builtin/labels/inventree_machine.py | 16 ++-- .../InvenTree/plugin/machine/__init__.py | 9 +- src/backend/InvenTree/plugin/registry.py | 9 ++ .../integration/test_scheduled_task.py | 5 +- .../src/tables/machine/MachineListTable.tsx | 2 +- 7 files changed, 118 insertions(+), 16 deletions(-) diff --git a/src/backend/InvenTree/machine/machine_type.py b/src/backend/InvenTree/machine/machine_type.py index 1b8bdf4133..83c1801bba 100644 --- a/src/backend/InvenTree/machine/machine_type.py +++ b/src/backend/InvenTree/machine/machine_type.py @@ -233,17 +233,21 @@ class BaseMachineType( # always fetch the machine_config if needed to ensure we get the newest reference from .models import MachineConfig - return MachineConfig.objects.get(pk=self.pk) + return MachineConfig.objects.filter(pk=self.pk).first() @property def name(self): """The machines name.""" - return self.machine_config.name + if config := self.machine_config: + return config.name @property def active(self): """The machines active status.""" - return self.machine_config.active + if config := self.machine_config: + return config.active + + return False # --- hook functions def initialize(self): diff --git a/src/backend/InvenTree/machine/registry.py b/src/backend/InvenTree/machine/registry.py index be8aeb677a..39fd5b3964 100644 --- a/src/backend/InvenTree/machine/registry.py +++ b/src/backend/InvenTree/machine/registry.py @@ -5,7 +5,9 @@ from typing import Union, cast from uuid import UUID from django.core.cache import cache +from django.db.utils import IntegrityError, OperationalError, ProgrammingError +from common.settings import get_global_setting, set_global_setting from InvenTree.helpers_mixin import get_shared_class_instance_state_mixin from machine.machine_type import BaseDriver, BaseMachineType @@ -29,6 +31,7 @@ class MachineRegistry( self.base_drivers: list[type[BaseDriver]] = [] + # Keep an internal hash of the machine registry state self._hash = None @property @@ -266,8 +269,14 @@ class MachineRegistry( """Calculate a hash of the machine registry state.""" from hashlib import md5 + from plugin import registry as plugin_registry + data = md5() + # If the plugin registry has changed, the machine registry hash will change + plugin_registry.update_plugin_hash() + data.update(plugin_registry.registry_hash.encode()) + for pk, machine in self.machines.items(): data.update(str(pk).encode()) try: @@ -283,16 +292,84 @@ class MachineRegistry( if not self._hash: self._hash = self._calculate_registry_hash() - last_hash = self.get_shared_state('hash', None) + try: + reg_hash = get_global_setting('_MACHINE_REGISTRY_HASH', '', create=False) + except Exception as exc: + logger.exception('Failed to get machine registry hash: %s', str(exc)) + return False - if last_hash and last_hash != self._hash: + if reg_hash and reg_hash != self._hash: logger.info('Machine registry has changed - reloading machines') self.reload_machines() + return True + + return False def _update_registry_hash(self): """Save the current registry hash.""" self._hash = self._calculate_registry_hash() - self.set_shared_state('hash', self._hash) + + try: + old_hash = get_global_setting('_MACHINE_REGISTRY_HASH') + except Exception: + old_hash = None + + if old_hash != self._hash: + try: + logger.info('Updating machine registry hash: %s', str(self._hash)) + set_global_setting('_MACHINE_REGISTRY_HASH', self._hash) + except (IntegrityError, OperationalError, ProgrammingError): + pass + except Exception as exc: + logger.exception('Failed to update machine registry hash: %s', str(exc)) + + def call_machine_function( + self, machine_id: str, function_name: str, *args, **kwargs + ): + """Call a named function against a machine instance. + + Arguments: + machine_id: The UUID of the machine to call the function against + function_name: The name of the function to call + """ + logger.info('call_machine_function: %s -> %s', machine_id, function_name) + + raise_error = kwargs.pop('raise_error', True) + + self._check_reload() + + # Fetch the machine instance based on the provided UUID + machine = self.get_machine(machine_id) + + if not machine: + if raise_error: + raise AttributeError(f"Machine '{machine_id}' not found") + return + + # Fetch the driver instance based on the machine driver + driver = machine.driver + + if not driver: + if raise_error: + raise AttributeError(f"Machine '{machine_id}' has no specified driver") + return + + # The function must be registered against the driver + func = getattr(driver, function_name) + + if not func or not callable(func): + if raise_error: + raise AttributeError( + f"Driver '{driver.SLUG}' has no callable method '{function_name}'" + ) + return + + return func(machine, *args, **kwargs) registry: MachineRegistry = MachineRegistry() + + +def call_machine_function(machine_id: str, function: str, *args, **kwargs): + """Global helper function to call a specific function on a machine instance.""" + return registry.call_machine_function(machine_id, function, *args, **kwargs) diff --git a/src/backend/InvenTree/plugin/builtin/labels/inventree_machine.py b/src/backend/InvenTree/plugin/builtin/labels/inventree_machine.py index 86ebcd71ec..9c8e9dba24 100644 --- a/src/backend/InvenTree/plugin/builtin/labels/inventree_machine.py +++ b/src/backend/InvenTree/plugin/builtin/labels/inventree_machine.py @@ -2,6 +2,7 @@ from typing import cast +from django.conf import settings from django.http import JsonResponse from django.utils.translation import gettext_lazy as _ @@ -12,7 +13,7 @@ from InvenTree.serializers import DependentField from InvenTree.tasks import offload_task from machine.machine_types import LabelPrinterBaseDriver, LabelPrinterMachine from plugin import InvenTreePlugin -from plugin.machine import registry +from plugin.machine import call_machine_function, registry from plugin.mixins import LabelPrintingMixin from report.models import LabelTemplate @@ -91,12 +92,15 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin): user=request.user, ) - # execute the print job - if driver.USE_BACKGROUND_WORKER is False: - return driver.print_labels(machine, label, items, **print_kwargs) - offload_task( - driver.print_labels, machine, label, items, group='plugin', **print_kwargs + call_machine_function, + machine.pk, + 'print_labels', + label, + items, + force_sync=settings.TESTING or driver.USE_BACKGROUND_WORKER, + group='plugin', + **print_kwargs, ) return JsonResponse({ diff --git a/src/backend/InvenTree/plugin/machine/__init__.py b/src/backend/InvenTree/plugin/machine/__init__.py index 3b11727bea..ec2750a58e 100644 --- a/src/backend/InvenTree/plugin/machine/__init__.py +++ b/src/backend/InvenTree/plugin/machine/__init__.py @@ -1,3 +1,10 @@ from machine import BaseDriver, BaseMachineType, MachineStatus, registry +from machine.registry import call_machine_function -__all__ = ['BaseDriver', 'BaseMachineType', 'MachineStatus', 'registry'] +__all__ = [ + 'BaseDriver', + 'BaseMachineType', + 'MachineStatus', + 'call_machine_function', + 'registry', +] diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index 1b8ec9403d..44a97706f1 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -170,13 +170,22 @@ class PluginsRegistry: # Check if the registry needs to be reloaded self.check_reload() + raise_error = kwargs.pop('raise_error', True) + plugin = self.get_plugin(slug) if not plugin: + if raise_error: + raise AttributeError(f"Plugin '{slug}' not found") return plugin_func = getattr(plugin, func) + if not plugin_func or not callable(plugin_func): + if raise_error: + raise AttributeError(f"Plugin '{slug}' has no callable method '{func}'") + return + return plugin_func(*args, **kwargs) # region registry functions diff --git a/src/backend/InvenTree/plugin/samples/integration/test_scheduled_task.py b/src/backend/InvenTree/plugin/samples/integration/test_scheduled_task.py index b2426a4e06..2b9d254456 100644 --- a/src/backend/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/src/backend/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -65,12 +65,13 @@ class ExampleScheduledTaskPluginTests(TestCase): self.assertEqual(len(scheduled_plugin_tasks), 0) def test_calling(self): - """Check if a function can be called without errors.""" + """Test calling of plugin functions by name.""" # Check with right parameters self.assertEqual(call_plugin_function('schedule', 'member_func'), False) # Check with wrong key - self.assertEqual(call_plugin_function('does_not_exist', 'member_func'), None) + with self.assertRaises(AttributeError): + call_plugin_function('does_not_exist', 'member_func'), None class ScheduledTaskPluginTests(TestCase): diff --git a/src/frontend/src/tables/machine/MachineListTable.tsx b/src/frontend/src/tables/machine/MachineListTable.tsx index c6f13a9ed0..0b8716b50c 100644 --- a/src/frontend/src/tables/machine/MachineListTable.tsx +++ b/src/frontend/src/tables/machine/MachineListTable.tsx @@ -511,7 +511,7 @@ export function MachineListTable({ }, [machineDrivers, createFormMachineType]); const createMachineForm = useCreateApiFormModal({ - title: t`Add machine`, + title: t`Add Machine`, url: ApiEndpoints.machine_list, fields: { name: {},