2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

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
This commit is contained in:
Oliver 2025-03-14 13:40:37 +11:00 committed by GitHub
parent 75420fc97e
commit af0a2822d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 118 additions and 16 deletions

View File

@ -233,17 +233,21 @@ class BaseMachineType(
# always fetch the machine_config if needed to ensure we get the newest reference # always fetch the machine_config if needed to ensure we get the newest reference
from .models import MachineConfig from .models import MachineConfig
return MachineConfig.objects.get(pk=self.pk) return MachineConfig.objects.filter(pk=self.pk).first()
@property @property
def name(self): def name(self):
"""The machines name.""" """The machines name."""
return self.machine_config.name if config := self.machine_config:
return config.name
@property @property
def active(self): def active(self):
"""The machines active status.""" """The machines active status."""
return self.machine_config.active if config := self.machine_config:
return config.active
return False
# --- hook functions # --- hook functions
def initialize(self): def initialize(self):

View File

@ -5,7 +5,9 @@ from typing import Union, cast
from uuid import UUID from uuid import UUID
from django.core.cache import cache 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 InvenTree.helpers_mixin import get_shared_class_instance_state_mixin
from machine.machine_type import BaseDriver, BaseMachineType from machine.machine_type import BaseDriver, BaseMachineType
@ -29,6 +31,7 @@ class MachineRegistry(
self.base_drivers: list[type[BaseDriver]] = [] self.base_drivers: list[type[BaseDriver]] = []
# Keep an internal hash of the machine registry state
self._hash = None self._hash = None
@property @property
@ -266,8 +269,14 @@ class MachineRegistry(
"""Calculate a hash of the machine registry state.""" """Calculate a hash of the machine registry state."""
from hashlib import md5 from hashlib import md5
from plugin import registry as plugin_registry
data = md5() 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(): for pk, machine in self.machines.items():
data.update(str(pk).encode()) data.update(str(pk).encode())
try: try:
@ -283,16 +292,84 @@ class MachineRegistry(
if not self._hash: if not self._hash:
self._hash = self._calculate_registry_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') logger.info('Machine registry has changed - reloading machines')
self.reload_machines() self.reload_machines()
return True
return False
def _update_registry_hash(self): def _update_registry_hash(self):
"""Save the current registry hash.""" """Save the current registry hash."""
self._hash = self._calculate_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() 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)

View File

@ -2,6 +2,7 @@
from typing import cast from typing import cast
from django.conf import settings
from django.http import JsonResponse from django.http import JsonResponse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -12,7 +13,7 @@ from InvenTree.serializers import DependentField
from InvenTree.tasks import offload_task from InvenTree.tasks import offload_task
from machine.machine_types import LabelPrinterBaseDriver, LabelPrinterMachine from machine.machine_types import LabelPrinterBaseDriver, LabelPrinterMachine
from plugin import InvenTreePlugin from plugin import InvenTreePlugin
from plugin.machine import registry from plugin.machine import call_machine_function, registry
from plugin.mixins import LabelPrintingMixin from plugin.mixins import LabelPrintingMixin
from report.models import LabelTemplate from report.models import LabelTemplate
@ -91,12 +92,15 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin):
user=request.user, 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( 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({ return JsonResponse({

View File

@ -1,3 +1,10 @@
from machine import BaseDriver, BaseMachineType, MachineStatus, registry 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',
]

View File

@ -170,13 +170,22 @@ class PluginsRegistry:
# Check if the registry needs to be reloaded # Check if the registry needs to be reloaded
self.check_reload() self.check_reload()
raise_error = kwargs.pop('raise_error', True)
plugin = self.get_plugin(slug) plugin = self.get_plugin(slug)
if not plugin: if not plugin:
if raise_error:
raise AttributeError(f"Plugin '{slug}' not found")
return return
plugin_func = getattr(plugin, func) 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) return plugin_func(*args, **kwargs)
# region registry functions # region registry functions

View File

@ -65,12 +65,13 @@ class ExampleScheduledTaskPluginTests(TestCase):
self.assertEqual(len(scheduled_plugin_tasks), 0) self.assertEqual(len(scheduled_plugin_tasks), 0)
def test_calling(self): def test_calling(self):
"""Check if a function can be called without errors.""" """Test calling of plugin functions by name."""
# Check with right parameters # Check with right parameters
self.assertEqual(call_plugin_function('schedule', 'member_func'), False) self.assertEqual(call_plugin_function('schedule', 'member_func'), False)
# Check with wrong key # 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): class ScheduledTaskPluginTests(TestCase):

View File

@ -511,7 +511,7 @@ export function MachineListTable({
}, [machineDrivers, createFormMachineType]); }, [machineDrivers, createFormMachineType]);
const createMachineForm = useCreateApiFormModal({ const createMachineForm = useCreateApiFormModal({
title: t`Add machine`, title: t`Add Machine`,
url: ApiEndpoints.machine_list, url: ApiEndpoints.machine_list,
fields: { fields: {
name: {}, name: {},