Fix: Tag Interface Delete Button #104256

Manually merged
Sybren A. Stüvel merged 39 commits from Evelinealy/flamenco:tag-interface into main 2023-11-02 16:13:14 +01:00
32 changed files with 806 additions and 174 deletions
Showing only changes of commit 662160835a - Show all commits

View File

@ -21,6 +21,7 @@ bugs in actually-released versions.
- The webapp automatically reloads after a disconnect, when it reconnects to Flamenco Manager and sees the Manager version changed [#104235](https://projects.blender.org/studio/flamenco/pulls/104235). - The webapp automatically reloads after a disconnect, when it reconnects to Flamenco Manager and sees the Manager version changed [#104235](https://projects.blender.org/studio/flamenco/pulls/104235).
- Show the configured Flamenco Manager name in the webapp's browser window title. - Show the configured Flamenco Manager name in the webapp's browser window title.
- Workers can be marked as 'restartable' by using the `-restart-exit-code N` commandline option. More info in the [Worker Actions documentation](https://flamenco.blender.org/usage/worker-actions/). - Workers can be marked as 'restartable' by using the `-restart-exit-code N` commandline option. More info in the [Worker Actions documentation](https://flamenco.blender.org/usage/worker-actions/).
- The `{timestamp}` placeholder in the render output path is now replaced with a local timestamp (rather than UTC).
## 3.2 - released 2023-02-21 ## 3.2 - released 2023-02-21

View File

@ -71,6 +71,9 @@ stresser:
job-creator: job-creator:
go build -v ${BUILD_FLAGS} ${PKG}/cmd/job-creator go build -v ${BUILD_FLAGS} ${PKG}/cmd/job-creator
flamenco-addon.zip: addon-packer
./addon-packer -filename ./flamenco-addon.zip
addon-packer: cmd/addon-packer/addon-packer.go addon-packer: cmd/addon-packer/addon-packer.go
go build -v ${BUILD_FLAGS} ${PKG}/cmd/addon-packer go build -v ${BUILD_FLAGS} ${PKG}/cmd/addon-packer

View File

@ -5,7 +5,7 @@ What kind of thing to subscribe to / unsubscribe from.
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**value** | **str** | What kind of thing to subscribe to / unsubscribe from. | must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", ] **value** | **str** | What kind of thing to subscribe to / unsubscribe from. | must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", "allWorkerTags", ]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -0,0 +1,14 @@
# SocketIOWorkerTagUpdate
Worker Tag, sent over SocketIO when it changes.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**tag** | [**WorkerTag**](WorkerTag.md) | |
**was_deleted** | **bool** | When a tag was just deleted, this is set to `true`. | [optional]
**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -57,6 +57,7 @@ class SocketIOSubscriptionType(ModelSimple):
'JOB': "job", 'JOB': "job",
'TASKLOG': "tasklog", 'TASKLOG': "tasklog",
'ALLLASTRENDERED': "allLastRendered", 'ALLLASTRENDERED': "allLastRendered",
'ALLWORKERTAGS': "allWorkerTags",
}, },
} }
@ -108,10 +109,10 @@ class SocketIOSubscriptionType(ModelSimple):
Note that value can be passed either in args or in kwargs, but not in both. Note that value can be passed either in args or in kwargs, but not in both.
Args: Args:
args[0] (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", ] # noqa: E501 args[0] (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", "allWorkerTags", ] # noqa: E501
Keyword Args: Keyword Args:
value (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", ] # noqa: E501 value (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", "allWorkerTags", ] # noqa: E501
_check_type (bool): if True, values for parameters in openapi_types _check_type (bool): if True, values for parameters in openapi_types
will be type checked and a TypeError will be will be type checked and a TypeError will be
raised if the wrong type is input. raised if the wrong type is input.
@ -198,10 +199,10 @@ class SocketIOSubscriptionType(ModelSimple):
Note that value can be passed either in args or in kwargs, but not in both. Note that value can be passed either in args or in kwargs, but not in both.
Args: Args:
args[0] (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", ] # noqa: E501 args[0] (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", "allWorkerTags", ] # noqa: E501
Keyword Args: Keyword Args:
value (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", ] # noqa: E501 value (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", "allWorkerTags", ] # noqa: E501
_check_type (bool): if True, values for parameters in openapi_types _check_type (bool): if True, values for parameters in openapi_types
will be type checked and a TypeError will be will be type checked and a TypeError will be
raised if the wrong type is input. raised if the wrong type is input.

View File

@ -0,0 +1,271 @@
"""
Flamenco manager
Render Farm manager API # noqa: E501
The version of the OpenAPI document: 1.0.0
Generated by: https://openapi-generator.tech
"""
import re # noqa: F401
import sys # noqa: F401
from flamenco.manager.model_utils import ( # noqa: F401
ApiTypeError,
ModelComposed,
ModelNormal,
ModelSimple,
cached_property,
change_keys_js_to_python,
convert_js_args_to_python_args,
date,
datetime,
file_type,
none_type,
validate_get_composed_info,
OpenApiModel
)
from flamenco.manager.exceptions import ApiAttributeError
def lazy_import():
from flamenco.manager.model.worker_tag import WorkerTag
globals()['WorkerTag'] = WorkerTag
class SocketIOWorkerTagUpdate(ModelNormal):
"""NOTE: This class is auto generated by OpenAPI Generator.
Ref: https://openapi-generator.tech
Do not edit the class manually.
Attributes:
allowed_values (dict): The key is the tuple path to the attribute
and the for var_name this is (var_name,). The value is a dict
with a capitalized key describing the allowed value and an allowed
value. These dicts store the allowed enum values.
attribute_map (dict): The key is attribute name
and the value is json key in definition.
discriminator_value_class_map (dict): A dict to go from the discriminator
variable value to the discriminator class name.
validations (dict): The key is the tuple path to the attribute
and the for var_name this is (var_name,). The value is a dict
that stores validations for max_length, min_length, max_items,
min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
inclusive_minimum, and regex.
additional_properties_type (tuple): A tuple of classes accepted
as additional properties values.
"""
allowed_values = {
}
validations = {
}
@cached_property
def additional_properties_type():
"""
This must be a method because a model may have properties that are
of type self, this must run after the class is loaded
"""
lazy_import()
return (bool, date, datetime, dict, float, int, list, str, none_type,) # noqa: E501
_nullable = False
@cached_property
def openapi_types():
"""
This must be a method because a model may have properties that are
of type self, this must run after the class is loaded
Returns
openapi_types (dict): The key is attribute name
and the value is attribute type.
"""
lazy_import()
return {
'tag': (WorkerTag,), # noqa: E501
'was_deleted': (bool,), # noqa: E501
}
@cached_property
def discriminator():
return None
attribute_map = {
'tag': 'tag', # noqa: E501
'was_deleted': 'was_deleted', # noqa: E501
}
read_only_vars = {
}
_composed_schemas = {}
@classmethod
@convert_js_args_to_python_args
def _from_openapi_data(cls, tag, *args, **kwargs): # noqa: E501
"""SocketIOWorkerTagUpdate - a model defined in OpenAPI
Args:
tag (WorkerTag):
Keyword Args:
_check_type (bool): if True, values for parameters in openapi_types
will be type checked and a TypeError will be
raised if the wrong type is input.
Defaults to True
_path_to_item (tuple/list): This is a list of keys or values to
drill down to the model in received_data
when deserializing a response
_spec_property_naming (bool): True if the variable names in the input data
are serialized names, as specified in the OpenAPI document.
False if the variable names in the input data
are pythonic names, e.g. snake case (default)
_configuration (Configuration): the instance to use when
deserializing a file_type parameter.
If passed, type conversion is attempted
If omitted no type conversion is done.
_visited_composed_classes (tuple): This stores a tuple of
classes that we have traveled through so that
if we see that class again we will not use its
discriminator again.
When traveling through a discriminator, the
composed schema that is
is traveled through is added to this set.
For example if Animal has a discriminator
petType and we pass in "Dog", and the class Dog
allOf includes Animal, we move through Animal
once using the discriminator, and pick Dog.
Then in Dog, we will make an instance of the
Animal class but this time we won't travel
through its discriminator because we passed in
_visited_composed_classes = (Animal,)
was_deleted (bool): When a tag was just deleted, this is set to `true`.. [optional] # noqa: E501
"""
_check_type = kwargs.pop('_check_type', True)
_spec_property_naming = kwargs.pop('_spec_property_naming', False)
_path_to_item = kwargs.pop('_path_to_item', ())
_configuration = kwargs.pop('_configuration', None)
_visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
self = super(OpenApiModel, cls).__new__(cls)
if args:
raise ApiTypeError(
"Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
args,
self.__class__.__name__,
),
path_to_item=_path_to_item,
valid_classes=(self.__class__,),
)
self._data_store = {}
self._check_type = _check_type
self._spec_property_naming = _spec_property_naming
self._path_to_item = _path_to_item
self._configuration = _configuration
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
self.tag = tag
for var_name, var_value in kwargs.items():
if var_name not in self.attribute_map and \
self._configuration is not None and \
self._configuration.discard_unknown_keys and \
self.additional_properties_type is None:
# discard variable.
continue
setattr(self, var_name, var_value)
return self
required_properties = set([
'_data_store',
'_check_type',
'_spec_property_naming',
'_path_to_item',
'_configuration',
'_visited_composed_classes',
])
@convert_js_args_to_python_args
def __init__(self, tag, *args, **kwargs): # noqa: E501
"""SocketIOWorkerTagUpdate - a model defined in OpenAPI
Args:
tag (WorkerTag):
Keyword Args:
_check_type (bool): if True, values for parameters in openapi_types
will be type checked and a TypeError will be
raised if the wrong type is input.
Defaults to True
_path_to_item (tuple/list): This is a list of keys or values to
drill down to the model in received_data
when deserializing a response
_spec_property_naming (bool): True if the variable names in the input data
are serialized names, as specified in the OpenAPI document.
False if the variable names in the input data
are pythonic names, e.g. snake case (default)
_configuration (Configuration): the instance to use when
deserializing a file_type parameter.
If passed, type conversion is attempted
If omitted no type conversion is done.
_visited_composed_classes (tuple): This stores a tuple of
classes that we have traveled through so that
if we see that class again we will not use its
discriminator again.
When traveling through a discriminator, the
composed schema that is
is traveled through is added to this set.
For example if Animal has a discriminator
petType and we pass in "Dog", and the class Dog
allOf includes Animal, we move through Animal
once using the discriminator, and pick Dog.
Then in Dog, we will make an instance of the
Animal class but this time we won't travel
through its discriminator because we passed in
_visited_composed_classes = (Animal,)
was_deleted (bool): When a tag was just deleted, this is set to `true`.. [optional] # noqa: E501
"""
_check_type = kwargs.pop('_check_type', True)
_spec_property_naming = kwargs.pop('_spec_property_naming', False)
_path_to_item = kwargs.pop('_path_to_item', ())
_configuration = kwargs.pop('_configuration', None)
_visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
if args:
raise ApiTypeError(
"Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
args,
self.__class__.__name__,
),
path_to_item=_path_to_item,
valid_classes=(self.__class__,),
)
self._data_store = {}
self._check_type = _check_type
self._spec_property_naming = _spec_property_naming
self._path_to_item = _path_to_item
self._configuration = _configuration
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
self.tag = tag
for var_name, var_value in kwargs.items():
if var_name not in self.attribute_map and \
self._configuration is not None and \
self._configuration.discard_unknown_keys and \
self.additional_properties_type is None:
# discard variable.
continue
setattr(self, var_name, var_value)
if var_name in self.read_only_vars:
raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
f"class with read only attributes.")

View File

@ -64,6 +64,7 @@ from flamenco.manager.model.socket_io_subscription_operation import SocketIOSubs
from flamenco.manager.model.socket_io_subscription_type import SocketIOSubscriptionType from flamenco.manager.model.socket_io_subscription_type import SocketIOSubscriptionType
from flamenco.manager.model.socket_io_task_log_update import SocketIOTaskLogUpdate from flamenco.manager.model.socket_io_task_log_update import SocketIOTaskLogUpdate
from flamenco.manager.model.socket_io_task_update import SocketIOTaskUpdate from flamenco.manager.model.socket_io_task_update import SocketIOTaskUpdate
from flamenco.manager.model.socket_io_worker_tag_update import SocketIOWorkerTagUpdate
from flamenco.manager.model.socket_io_worker_update import SocketIOWorkerUpdate from flamenco.manager.model.socket_io_worker_update import SocketIOWorkerUpdate
from flamenco.manager.model.submitted_job import SubmittedJob from flamenco.manager.model.submitted_job import SubmittedJob
from flamenco.manager.model.task import Task from flamenco.manager.model.task import Task

7
go.mod
View File

@ -7,8 +7,8 @@ require (
github.com/benbjohnson/clock v1.3.0 github.com/benbjohnson/clock v1.3.0
github.com/deepmap/oapi-codegen v1.9.0 github.com/deepmap/oapi-codegen v1.9.0
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/dop251/goja v0.0.0-20211217115348-3f9136fa235d github.com/dop251/goja v0.0.0-20230812105242-81d76064690d
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7 github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d
github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e
github.com/gertd/go-pluralize v0.2.1 github.com/gertd/go-pluralize v0.2.1
github.com/getkin/kin-openapi v0.88.0 github.com/getkin/kin-openapi v0.88.0
@ -34,13 +34,14 @@ require (
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.19.5 // indirect github.com/go-openapi/swag v0.19.5 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect

15
go.sum
View File

@ -2,6 +2,9 @@ github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
@ -17,10 +20,17 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20211217115348-3f9136fa235d h1:XT7Qdmcuwgsgz4GXejX7R5Morysk2GOpeguYJ9JoF5c= github.com/dop251/goja v0.0.0-20211217115348-3f9136fa235d h1:XT7Qdmcuwgsgz4GXejX7R5Morysk2GOpeguYJ9JoF5c=
github.com/dop251/goja v0.0.0-20211217115348-3f9136fa235d/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20211217115348-3f9136fa235d/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20230812105242-81d76064690d h1:9aaGwVf4q+kknu+mROAXUApJ1DoOwhE8dGj/XLBYzWg=
github.com/dop251/goja v0.0.0-20230812105242-81d76064690d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7 h1:tYwu/z8Y0NkkzGEh3z21mSWggMg4LwLRFucLS7TjARg= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7 h1:tYwu/z8Y0NkkzGEh3z21mSWggMg4LwLRFucLS7TjARg=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d h1:W1n4DvpzZGOISgp7wWNtraLcHtnmnTwBlJidqtMIuwQ=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
@ -71,6 +81,8 @@ github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGS
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@ -81,6 +93,7 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graarh/golang-socketio v0.0.0-20170510162725-2c44953b9b5f h1:utzdm9zUvVWGRtIpkdE4+36n+Gv60kNb7mFvgGxLElY= github.com/graarh/golang-socketio v0.0.0-20170510162725-2c44953b9b5f h1:utzdm9zUvVWGRtIpkdE4+36n+Gv60kNb7mFvgGxLElY=
github.com/graarh/golang-socketio v0.0.0-20170510162725-2c44953b9b5f/go.mod h1:8gudiNCFh3ZfvInknmoXzPeV17FSH+X2J5k2cUPIwnA= github.com/graarh/golang-socketio v0.0.0-20170510162725-2c44953b9b5f/go.mod h1:8gudiNCFh3ZfvInknmoXzPeV17FSH+X2J5k2cUPIwnA=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@ -241,6 +254,7 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -255,6 +269,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -120,6 +120,9 @@ type ChangeBroadcaster interface {
BroadcastWorkerUpdate(workerUpdate api.SocketIOWorkerUpdate) BroadcastWorkerUpdate(workerUpdate api.SocketIOWorkerUpdate)
BroadcastNewWorker(workerUpdate api.SocketIOWorkerUpdate) BroadcastNewWorker(workerUpdate api.SocketIOWorkerUpdate)
BroadcastWorkerTagUpdate(workerTagUpdate api.SocketIOWorkerTagUpdate)
BroadcastNewWorkerTag(workerTagUpdate api.SocketIOWorkerTagUpdate)
} }
// ChangeBroadcaster should be a subset of webupdates.BiDirComms. // ChangeBroadcaster should be a subset of webupdates.BiDirComms.

View File

@ -632,6 +632,30 @@ func (mr *MockChangeBroadcasterMockRecorder) BroadcastNewWorker(arg0 interface{}
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastNewWorker", reflect.TypeOf((*MockChangeBroadcaster)(nil).BroadcastNewWorker), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastNewWorker", reflect.TypeOf((*MockChangeBroadcaster)(nil).BroadcastNewWorker), arg0)
} }
// BroadcastNewWorkerTag mocks base method.
func (m *MockChangeBroadcaster) BroadcastNewWorkerTag(arg0 api.SocketIOWorkerTagUpdate) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "BroadcastNewWorkerTag", arg0)
}
// BroadcastNewWorkerTag indicates an expected call of BroadcastNewWorkerTag.
func (mr *MockChangeBroadcasterMockRecorder) BroadcastNewWorkerTag(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastNewWorkerTag", reflect.TypeOf((*MockChangeBroadcaster)(nil).BroadcastNewWorkerTag), arg0)
}
// BroadcastWorkerTagUpdate mocks base method.
func (m *MockChangeBroadcaster) BroadcastWorkerTagUpdate(arg0 api.SocketIOWorkerTagUpdate) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "BroadcastWorkerTagUpdate", arg0)
}
// BroadcastWorkerTagUpdate indicates an expected call of BroadcastWorkerTagUpdate.
func (mr *MockChangeBroadcasterMockRecorder) BroadcastWorkerTagUpdate(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastWorkerTagUpdate", reflect.TypeOf((*MockChangeBroadcaster)(nil).BroadcastWorkerTagUpdate), arg0)
}
// BroadcastWorkerUpdate mocks base method. // BroadcastWorkerUpdate mocks base method.
func (m *MockChangeBroadcaster) BroadcastWorkerUpdate(arg0 api.SocketIOWorkerUpdate) { func (m *MockChangeBroadcaster) BroadcastWorkerUpdate(arg0 api.SocketIOWorkerUpdate) {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -266,7 +266,9 @@ func (f *Flamenco) DeleteWorkerTag(e echo.Context, tagUUID string) error {
return sendAPIError(e, http.StatusInternalServerError, "error deleting worker tag: %v", err) return sendAPIError(e, http.StatusInternalServerError, "error deleting worker tag: %v", err)
} }
// TODO: SocketIO broadcast of tag deletion. // SocketIO broadcast of tag deletion.
update := webupdates.NewWorkerTagDeletedUpdate(tagUUID)
f.broadcaster.BroadcastWorkerTagUpdate(update)
logger.Info().Msg("worker tag deleted") logger.Info().Msg("worker tag deleted")
return e.NoContent(http.StatusNoContent) return e.NoContent(http.StatusNoContent)
@ -344,7 +346,10 @@ func (f *Flamenco) UpdateWorkerTag(e echo.Context, tagUUID string) error {
return sendAPIError(e, http.StatusInternalServerError, "error saving worker tag") return sendAPIError(e, http.StatusInternalServerError, "error saving worker tag")
} }
// TODO: SocketIO broadcast of tag update. // SocketIO broadcast of tag update.
sioUpdate := webupdates.NewWorkerTagUpdate(dbTag)
f.broadcaster.BroadcastWorkerTagUpdate(sioUpdate)
logger.Info().Msg("worker tag updated") logger.Info().Msg("worker tag updated")
return e.NoContent(http.StatusNoContent) return e.NoContent(http.StatusNoContent)
} }
@ -412,7 +417,10 @@ func (f *Flamenco) CreateWorkerTag(e echo.Context) error {
} }
logger.Info().Msg("created new worker tag") logger.Info().Msg("created new worker tag")
// TODO: SocketIO broadcast of tag creation.
// SocketIO broadcast of tag creation.
sioUpdate := webupdates.NewWorkerTagUpdate(&dbTag)
f.broadcaster.BroadcastNewWorkerTag(sioUpdate)
return e.JSON(http.StatusOK, workerTagDBtoAPI(dbTag)) return e.JSON(http.StatusOK, workerTagDBtoAPI(dbTag))
} }

View File

@ -281,7 +281,9 @@ func TestWorkerTagCRUDHappyFlow(t *testing.T) {
Description: *apiTag.Description, Description: *apiTag.Description,
} }
mf.persistence.EXPECT().CreateWorkerTag(gomock.Any(), &expectDBTag) mf.persistence.EXPECT().CreateWorkerTag(gomock.Any(), &expectDBTag)
// TODO: expect SocketIO broadcast of the tag creation. mf.broadcaster.EXPECT().BroadcastNewWorkerTag(api.SocketIOWorkerTagUpdate{
Tag: apiTag,
})
echo := mf.prepareMockedJSONRequest(apiTag) echo := mf.prepareMockedJSONRequest(apiTag)
require.NoError(t, mf.flamenco.CreateWorkerTag(echo)) require.NoError(t, mf.flamenco.CreateWorkerTag(echo))
assertResponseJSON(t, echo, http.StatusOK, &apiTag) assertResponseJSON(t, echo, http.StatusOK, &apiTag)
@ -303,7 +305,13 @@ func TestWorkerTagCRUDHappyFlow(t *testing.T) {
Name: newAPITag.Name, Name: newAPITag.Name,
Description: *apiTag.Description, // Not mentioning new description should keep old one. Description: *apiTag.Description, // Not mentioning new description should keep old one.
} }
// TODO: expect SocketIO broadcast of the tag update. mf.broadcaster.EXPECT().BroadcastWorkerTagUpdate(api.SocketIOWorkerTagUpdate{
Tag: api.WorkerTag{
Id: &UUID,
Name: newAPITag.Name,
Description: apiTag.Description,
},
})
mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil) mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil)
mf.persistence.EXPECT().SaveWorkerTag(gomock.Any(), &expectNewDBTag) mf.persistence.EXPECT().SaveWorkerTag(gomock.Any(), &expectNewDBTag)
echo = mf.prepareMockedJSONRequest(newAPITag) echo = mf.prepareMockedJSONRequest(newAPITag)
@ -320,7 +328,13 @@ func TestWorkerTagCRUDHappyFlow(t *testing.T) {
Name: newAPITag.Name, Name: newAPITag.Name,
Description: "", Description: "",
} }
// TODO: expect SocketIO broadcast of the tag update. mf.broadcaster.EXPECT().BroadcastWorkerTagUpdate(api.SocketIOWorkerTagUpdate{
Tag: api.WorkerTag{
Id: &UUID,
Name: newAPITag.Name,
Description: newAPITag.Description,
},
})
mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil) mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil)
mf.persistence.EXPECT().SaveWorkerTag(gomock.Any(), &expectNewDBTag) mf.persistence.EXPECT().SaveWorkerTag(gomock.Any(), &expectNewDBTag)
echo = mf.prepareMockedJSONRequest(newAPITag) echo = mf.prepareMockedJSONRequest(newAPITag)
@ -337,7 +351,13 @@ func TestWorkerTagCRUDHappyFlow(t *testing.T) {
Name: newAPITag.Name, Name: newAPITag.Name,
Description: *newAPITag.Description, Description: *newAPITag.Description,
} }
// TODO: expect SocketIO broadcast of the tag update. mf.broadcaster.EXPECT().BroadcastWorkerTagUpdate(api.SocketIOWorkerTagUpdate{
Tag: api.WorkerTag{
Id: &UUID,
Name: newAPITag.Name,
Description: newAPITag.Description,
},
})
mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil) mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil)
mf.persistence.EXPECT().SaveWorkerTag(gomock.Any(), &expectNewDBTag) mf.persistence.EXPECT().SaveWorkerTag(gomock.Any(), &expectNewDBTag)
echo = mf.prepareMockedJSONRequest(newAPITag) echo = mf.prepareMockedJSONRequest(newAPITag)
@ -347,7 +367,10 @@ func TestWorkerTagCRUDHappyFlow(t *testing.T) {
// Delete. // Delete.
mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil) mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil)
mf.persistence.EXPECT().DeleteWorkerTag(gomock.Any(), UUID) mf.persistence.EXPECT().DeleteWorkerTag(gomock.Any(), UUID)
// TODO: expect SocketIO broadcast of the tag deletion. mf.broadcaster.EXPECT().BroadcastWorkerTagUpdate(api.SocketIOWorkerTagUpdate{
Tag: api.WorkerTag{Id: &UUID},
WasDeleted: ptr(true),
})
echo = mf.prepareMockedJSONRequest(newAPITag) echo = mf.prepareMockedJSONRequest(newAPITag)
require.NoError(t, mf.flamenco.DeleteWorkerTag(echo, UUID)) require.NoError(t, mf.flamenco.DeleteWorkerTag(echo, UUID))
assertResponseNoContent(t, echo) assertResponseNoContent(t, echo)

View File

@ -57,7 +57,7 @@ func exampleSubmittedJob() api.SubmittedJob {
func mockedClock(t *testing.T) clock.Clock { func mockedClock(t *testing.T) clock.Clock {
c := clock.NewMock() c := clock.NewMock()
now, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00") now, err := time.ParseInLocation("2006-01-02T15:04:05", "2006-01-02T15:04:05", time.Local)
assert.NoError(t, err) assert.NoError(t, err)
c.Set(now) c.Set(now)
return c return c
@ -250,13 +250,13 @@ func TestSimpleBlenderRenderOutputPathFieldReplacement(t *testing.T) {
require.NotNil(t, aj) require.NotNil(t, aj)
// The job compiler should have replaced the {timestamp} and {ext} fields. // The job compiler should have replaced the {timestamp} and {ext} fields.
assert.Equal(t, "/root/2006-01-02_090405/jobname/######", aj.Settings["render_output_path"]) assert.Equal(t, "/root/2006-01-02_150405/jobname/######", aj.Settings["render_output_path"])
// Tasks should have been created to render the frames: 1-3, 4-6, 7-9, 10, and video-encoding // Tasks should have been created to render the frames: 1-3, 4-6, 7-9, 10, and video-encoding
require.Len(t, aj.Tasks, 5) require.Len(t, aj.Tasks, 5)
t0 := aj.Tasks[0] t0 := aj.Tasks[0]
expectCliArgs := []interface{}{ // They are strings, but Goja doesn't know that and will produce an []interface{}. expectCliArgs := []interface{}{ // They are strings, but Goja doesn't know that and will produce an []interface{}.
"--render-output", "/root/2006-01-02_090405/jobname/######", "--render-output", "/root/2006-01-02_150405/jobname/######",
"--render-format", sj.Settings.AdditionalProperties["format"].(string), "--render-format", sj.Settings.AdditionalProperties["format"].(string),
"--render-frame", "1..3", "--render-frame", "1..3",
} }
@ -271,8 +271,8 @@ func TestSimpleBlenderRenderOutputPathFieldReplacement(t *testing.T) {
tVideo := aj.Tasks[4] // This should be a video encoding task tVideo := aj.Tasks[4] // This should be a video encoding task
assert.EqualValues(t, AuthoredCommandParameters{ assert.EqualValues(t, AuthoredCommandParameters{
"exe": "ffmpeg", "exe": "ffmpeg",
"inputGlob": "/root/2006-01-02_090405/jobname/*.png", "inputGlob": "/root/2006-01-02_150405/jobname/*.png",
"outputFile": "/root/2006-01-02_090405/jobname/scene123-1-10.mp4", "outputFile": "/root/2006-01-02_150405/jobname/scene123-1-10.mp4",
"fps": int64(24), "fps": int64(24),
"args": expectedFramesToVideoArgs, "args": expectedFramesToVideoArgs,
}, tVideo.Commands[0].Parameters) }, tVideo.Commands[0].Parameters)
@ -327,6 +327,57 @@ func TestEtag(t *testing.T) {
} }
} }
func TestComplexFrameRange(t *testing.T) {
s, err := Load(mockedClock(t))
require.NoError(t, err)
// Compiling a job should be really fast.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
sj := exampleSubmittedJob()
// Use a series of ranges, where each range is smaller than the chunk size.
sj.Settings.AdditionalProperties["frames"] = "0-12,34-56,78-90"
sj.Settings.AdditionalProperties["chunk_size"] = 20
aj, err := s.Compile(ctx, sj)
require.NoError(t, err)
require.NotNil(t, aj)
// Expected chunks:
// - 0-12, 34-40
// - 41-56, 78-81
// - 82-90
taskNames := []string{}
for _, task := range aj.Tasks {
taskNames = append(taskNames, task.Name)
}
require.Equal(t, []string{
"render-0-12,34-40",
"render-41-56,78-81",
"render-82-90",
"preview-video",
}, taskNames)
// Check the Blender CLI matches the expected frame ranges.
frameRangesFromCLI := []string{}
for _, task := range aj.Tasks[0:3] {
args := task.Commands[0].Parameters["args"].([]interface{})
require.Equal(t, "--render-frame", args[4])
frameRangesFromCLI = append(frameRangesFromCLI, args[5].(string))
}
assert.Equal(t,
[]string{
"0..12,34..40",
"41..56,78..81",
"82..90",
},
frameRangesFromCLI,
)
}
func ptr[T any](value T) *T { func ptr[T any](value T) *T {
return &value return &value
} }

View File

@ -125,7 +125,7 @@ function authorRenderTasks(settings, renderDir, renderOutput) {
args: [ args: [
"--render-output", path.join(renderDir, path.basename(renderOutput)), "--render-output", path.join(renderDir, path.basename(renderOutput)),
"--render-format", settings.format, "--render-format", settings.format,
"--render-frame", chunk.replace("-", ".."), // Convert to Blender frame range notation. "--render-frame", chunk.replaceAll("-", ".."), // Convert to Blender frame range notation.
] ]
}); });
task.addCommand(command); task.addCommand(command);

View File

@ -109,7 +109,7 @@ function authorRenderTasks(settings, renderDir, renderOutput) {
args: [ args: [
"--render-output", path.join(renderDir, path.basename(renderOutput)), "--render-output", path.join(renderDir, path.basename(renderOutput)),
"--render-format", settings.format, "--render-format", settings.format,
"--render-frame", chunk.replace("-", ".."), // Convert to Blender frame range notation. "--render-frame", chunk.replaceAll("-", ".."), // Convert to Blender frame range notation.
] ]
}); });
task.addCommand(command); task.addCommand(command);

View File

@ -60,7 +60,7 @@ func calculateNextCheck(now time.Time, schedule *persistence.SleepSchedule) time
// calcNext returns the given time of day on "today" if that hasn't passed // calcNext returns the given time of day on "today" if that hasn't passed
// yet, otherwise on "tomorrow". // yet, otherwise on "tomorrow".
calcNext := func(tod persistence.TimeOfDay) time.Time { calcNext := func(tod persistence.TimeOfDay) time.Time {
nextCheck := tod.OnDate(now) nextCheck := tod.OnDate(now).In(time.Local)
if nextCheck.Before(now) { if nextCheck.Before(now) {
nextCheck = nextCheck.AddDate(0, 0, 1) nextCheck = nextCheck.AddDate(0, 0, 1)
} }
@ -99,5 +99,6 @@ func earliestTime(timestamps []time.Time) time.Time {
// endOfDay returns the next midnight at UTC. // endOfDay returns the next midnight at UTC.
func endOfDay(now time.Time) time.Time { func endOfDay(now time.Time) time.Time {
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, 1) startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
return startOfDay.AddDate(0, 0, 1)
} }

View File

@ -256,13 +256,14 @@ type TestMocks struct {
// to the given time. Seconds and sub-seconds are set to zero. // to the given time. Seconds and sub-seconds are set to zero.
func (m *TestMocks) todayAt(hour, minute int) time.Time { func (m *TestMocks) todayAt(hour, minute int) time.Time {
now := m.clock.Now() now := m.clock.Now()
return time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location()) todayAt := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, time.Local)
return todayAt
} }
// endOfDay returns midnight of the day after whatever the mocked clock's "now" is set to. // endOfDay returns midnight of the day after whatever the mocked clock's "now" is set to.
func (m *TestMocks) endOfDay() time.Time { func (m *TestMocks) endOfDay() time.Time {
now := m.clock.Now().UTC() startOfToday := m.todayAt(0, 0)
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, 1) return startOfToday.AddDate(0, 0, 1)
} }
func testFixtures(t *testing.T) (*SleepScheduler, TestMocks, context.Context) { func testFixtures(t *testing.T) (*SleepScheduler, TestMocks, context.Context) {

View File

@ -24,6 +24,7 @@ const (
SocketIORoomChat SocketIORoomName = "Chat" // For chat messages. SocketIORoomChat SocketIORoomName = "Chat" // For chat messages.
SocketIORoomJobs SocketIORoomName = "Jobs" // For job updates. SocketIORoomJobs SocketIORoomName = "Jobs" // For job updates.
SocketIORoomWorkers SocketIORoomName = "Workers" // For worker updates. SocketIORoomWorkers SocketIORoomName = "Workers" // For worker updates.
SocketIORoomWorkerTags SocketIORoomName = "WorkerTags" // For worker tag updates.
// For updates about ALL last-rendered images. Normally these are sent to a // For updates about ALL last-rendered images. Normally these are sent to a
// room specific to a particular job, but for the global "last rendered image" // room specific to a particular job, but for the global "last rendered image"
@ -40,6 +41,7 @@ const (
SIOEventTaskUpdate SocketIOEventType = "/task" // sends api.SocketIOTaskUpdate SIOEventTaskUpdate SocketIOEventType = "/task" // sends api.SocketIOTaskUpdate
SIOEventTaskLogUpdate SocketIOEventType = "/tasklog" // sends api.SocketIOTaskLogUpdate SIOEventTaskLogUpdate SocketIOEventType = "/tasklog" // sends api.SocketIOTaskLogUpdate
SIOEventWorkerUpdate SocketIOEventType = "/workers" // sends api.SocketIOWorkerUpdate SIOEventWorkerUpdate SocketIOEventType = "/workers" // sends api.SocketIOWorkerUpdate
SIOEventWorkerTagUpdate SocketIOEventType = "/workertags" // sends api.SocketIOWorkerTagUpdate
SIOEventSubscription SocketIOEventType = "/subscription" // clients send api.SocketIOSubscription SIOEventSubscription SocketIOEventType = "/subscription" // clients send api.SocketIOSubscription
) )
@ -74,6 +76,8 @@ func (b *BiDirComms) handleRoomSubscription(c *gosocketio.Channel, subs api.Sock
sioRoom = SocketIORoomWorkers sioRoom = SocketIORoomWorkers
case api.SocketIOSubscriptionTypeAllLastRendered: case api.SocketIOSubscriptionTypeAllLastRendered:
sioRoom = SocketIORoomLastRendered sioRoom = SocketIORoomLastRendered
case api.SocketIOSubscriptionTypeAllWorkerTags:
sioRoom = SocketIORoomWorkerTags
case api.SocketIOSubscriptionTypeJob: case api.SocketIOSubscriptionTypeJob:
if subs.Uuid == nil { if subs.Uuid == nil {
logger.Warn().Msg("socketIO: trying to (un)subscribe to job without UUID") logger.Warn().Msg("socketIO: trying to (un)subscribe to job without UUID")

View File

@ -0,0 +1,48 @@
package webupdates
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"github.com/rs/zerolog/log"
"projects.blender.org/studio/flamenco/internal/manager/persistence"
"projects.blender.org/studio/flamenco/pkg/api"
)
// NewWorkerTagUpdate returns a partial SocketIOWorkerTagUpdate struct for the
// given worker tag. It only fills in the fields that represent the current
// state of the tag.
func NewWorkerTagUpdate(tag *persistence.WorkerTag) api.SocketIOWorkerTagUpdate {
tagUpdate := api.SocketIOWorkerTagUpdate{
Tag: api.WorkerTag{
Id: &tag.UUID,
Name: tag.Name,
Description: &tag.Description,
},
}
return tagUpdate
}
// NewWorkerTagDeletedUpdate returns a SocketIOWorkerTagUpdate struct that indicates
// the worker tag has been deleted.
func NewWorkerTagDeletedUpdate(tagUUID string) api.SocketIOWorkerTagUpdate {
wasDeleted := true
tagUpdate := api.SocketIOWorkerTagUpdate{
Tag: api.WorkerTag{
Id: &tagUUID,
},
WasDeleted: &wasDeleted,
}
return tagUpdate
}
// BroadcastWorkerTagUpdate sends the worker tag update to clients.
func (b *BiDirComms) BroadcastWorkerTagUpdate(WorkerTagUpdate api.SocketIOWorkerTagUpdate) {
log.Debug().Interface("WorkerTagUpdate", WorkerTagUpdate).Msg("socketIO: broadcasting worker tag update")
b.BroadcastTo(SocketIORoomWorkerTags, SIOEventWorkerTagUpdate, WorkerTagUpdate)
}
// BroadcastNewWorkerTag sends a "new worker tag" notification to clients.
func (b *BiDirComms) BroadcastNewWorkerTag(WorkerTagUpdate api.SocketIOWorkerTagUpdate) {
log.Debug().Interface("WorkerTagUpdate", WorkerTagUpdate).Msg("socketIO: broadcasting new worker tag")
b.BroadcastTo(SocketIORoomWorkerTags, SIOEventWorkerTagUpdate, WorkerTagUpdate)
}

View File

@ -12,7 +12,7 @@ import (
func mockedClock(t *testing.T) *clock.Mock { func mockedClock(t *testing.T) *clock.Mock {
c := clock.NewMock() c := clock.NewMock()
now, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00") now, err := time.ParseInLocation("2006-01-02T15:04:05", "2006-01-02T15:04:05", time.Local)
assert.NoError(t, err) assert.NoError(t, err)
c.Set(now) c.Set(now)
return c return c

View File

@ -2320,6 +2320,17 @@ components:
description: Whether this Worker can auto-restart. description: Whether this Worker can auto-restart.
required: [id, name, updated, status, version, can_restart] required: [id, name, updated, status, version, can_restart]
SocketIOWorkerTagUpdate:
type: object
description: >
Worker Tag, sent over SocketIO when it changes.
properties:
"tag": { $ref: "#/components/schemas/WorkerTag" }
"was_deleted":
type: boolean
description: When a tag was just deleted, this is set to `true`.
required: [tag]
SocketIOSubscription: SocketIOSubscription:
type: object type: object
description: > description: >
@ -2343,7 +2354,7 @@ components:
SocketIOSubscriptionType: SocketIOSubscriptionType:
type: string type: string
enum: [allJobs, allWorkers, job, tasklog, allLastRendered] enum: [allJobs, allWorkers, job, tasklog, allLastRendered, allWorkerTags]
description: What kind of thing to subscribe to / unsubscribe from. description: What kind of thing to subscribe to / unsubscribe from.
# Worker Management # Worker Management

View File

@ -108,134 +108,135 @@ var swaggerSpec = []string{
"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",
"z+HS9T/99un/BQAA//+MoGf5BQYBAA==", "w8uwJgLduXsbgmIzJrsNe1eULPyKOpOB0zFR6gXT58MswCfq3H8HwdAm7b2chIpm8RwuXf/Tb5/+XwAA",
"AP//gDoBPDoHAQA=",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

View File

@ -105,6 +105,8 @@ const (
SocketIOSubscriptionTypeAllLastRendered SocketIOSubscriptionType = "allLastRendered" SocketIOSubscriptionTypeAllLastRendered SocketIOSubscriptionType = "allLastRendered"
SocketIOSubscriptionTypeAllWorkerTags SocketIOSubscriptionType = "allWorkerTags"
SocketIOSubscriptionTypeAllWorkers SocketIOSubscriptionType = "allWorkers" SocketIOSubscriptionTypeAllWorkers SocketIOSubscriptionType = "allWorkers"
SocketIOSubscriptionTypeJob SocketIOSubscriptionType = "job" SocketIOSubscriptionTypeJob SocketIOSubscriptionType = "job"
@ -604,6 +606,15 @@ type SocketIOTaskUpdate struct {
Updated time.Time `json:"updated"` Updated time.Time `json:"updated"`
} }
// Worker Tag, sent over SocketIO when it changes.
type SocketIOWorkerTagUpdate struct {
// Tag of workers. A job can optionally specify which tag it should be limited to. Workers can be part of multiple tags simultaneously.
Tag WorkerTag `json:"tag"`
// When a tag was just deleted, this is set to `true`.
WasDeleted *bool `json:"was_deleted,omitempty"`
}
// Subset of a Worker, sent over SocketIO when a worker changes. // Subset of a Worker, sent over SocketIO when a worker changes.
type SocketIOWorkerUpdate struct { type SocketIOWorkerUpdate struct {
// Whether this Worker can auto-restart. // Whether this Worker can auto-restart.

View File

@ -13,12 +13,14 @@ const websocketURL = ws();
export default { export default {
emits: [ emits: [
// Data from Flamenco Manager: // Data from Flamenco Manager:
"jobUpdate", "taskUpdate", "taskLogUpdate", "message", "workerUpdate", "lastRenderedUpdate", "jobUpdate", "taskUpdate", "taskLogUpdate", "message", "workerUpdate",
"lastRenderedUpdate", "workerTagUpdate",
// SocketIO events: // SocketIO events:
"sioReconnected", "sioDisconnected" "sioReconnected", "sioDisconnected"
], ],
props: [ props: [
"mainSubscription", // One of the 'allXXX' subscription types, see `SocketIOSubscriptionType` in `flamenco-openapi.yaml`. "mainSubscription", // One of the 'allXXX' subscription types, see `SocketIOSubscriptionType` in `flamenco-openapi.yaml`.
"extraSubscription", // One of the 'allXXX' subscription types, see `SocketIOSubscriptionType` in `flamenco-openapi.yaml`.
"subscribedJobID", "subscribedJobID",
"subscribedTaskID", "subscribedTaskID",
], ],
@ -66,6 +68,14 @@ export default {
this._updateMainSubscription("subscribe", newType); this._updateMainSubscription("subscribe", newType);
} }
}, },
extraSubscription(newType, oldType) {
if (oldType) {
this._updateMainSubscription("unsubscribe", oldType);
}
if (newType) {
this._updateMainSubscription("subscribe", newType);
}
},
}, },
methods: { methods: {
connectToWebsocket() { connectToWebsocket() {
@ -160,6 +170,13 @@ export default {
this.$emit("workerUpdate", apiWorkerUpdate); this.$emit("workerUpdate", apiWorkerUpdate);
}); });
this.socket.on("/workertags", (workerTagUpdate) => {
// Convert to API object, in order to have the same parsing of data as
// when we'd do an API call.
const apiWorkerTagUpdate = API.SocketIOWorkerTagUpdate.constructFromObject(workerTagUpdate)
this.$emit("workerTagUpdate", apiWorkerTagUpdate);
});
// Chat system, useful for debugging. // Chat system, useful for debugging.
this.socket.on("/message", (message) => { this.socket.on("/message", (message) => {
this.$emit("message", message); this.$emit("message", message);
@ -219,6 +236,7 @@ export default {
if (this.subscribedJobID) this._updateJobSubscription("subscribe", this.subscribedJobID); if (this.subscribedJobID) this._updateJobSubscription("subscribe", this.subscribedJobID);
if (this.subscribedTaskID) this._updateTaskLogSubscription("subscribe", this.subscribedTaskID); if (this.subscribedTaskID) this._updateTaskLogSubscription("subscribe", this.subscribedTaskID);
if (this.mainSubscription) this._updateMainSubscription("subscribe", this.mainSubscription); if (this.mainSubscription) this._updateMainSubscription("subscribe", this.mainSubscription);
if (this.extraSubscription) this._updateMainSubscription("subscribe", this.extraSubscription);
}, },
}, },
}; };

View File

@ -97,6 +97,7 @@ import TabsWrapper from '@/components/TabsWrapper.vue'
import PopoverEditableJobPriority from '@/components/PopoverEditableJobPriority.vue' import PopoverEditableJobPriority from '@/components/PopoverEditableJobPriority.vue'
import { copyElementText, copyElementData } from '@/clipboard'; import { copyElementText, copyElementData } from '@/clipboard';
import { useWorkers } from '@/stores/workers' import { useWorkers } from '@/stores/workers'
import { useNotifs } from '@/stores/notifications';
export default { export default {
props: [ props: [
@ -123,6 +124,7 @@ export default {
jobTypeSettings: null, // Mapping from setting key to its definition in the job type. jobTypeSettings: null, // Mapping from setting key to its definition in the job type.
showAllSettings: false, showAllSettings: false,
workers: useWorkers(), workers: useWorkers(),
notifs: useNotifs(),
}; };
}, },
mounted() { mounted() {

View File

@ -123,17 +123,20 @@
<section class="worker-maintenance"> <section class="worker-maintenance">
<h3 class="sub-title">Maintenance</h3> <h3 class="sub-title">Maintenance</h3>
<p>{{ workerData.name }} is <span class="worker-status">{{ workerData.status }}</span>, which means <p>
<template v-if="workerData.status == 'offline'">can be safely removed.</template> {{ workerData.name }} is
<template v-else>removing it now can cause the Worker to log errors. It <template v-if="workerData.status == 'offline'">
is adviced to shut down the Worker before removing it from the <span class="worker-status">offline</span>, which means it can be safely removed.
system.</template> </template>
</p> <template v-else>
<p><button @click="deleteWorker">Remove {{ workerData.name }}</button></p> <template v-if="workerData.status == 'error'">
<p class="hint"> in <span class="worker-status">error</span> state
When a Worker is removed from the system, any active task still assigned </template>
to it will be requeued. Restarting the Worker after removing it from the <template v-else>
system will simply register it anew. <span class="worker-status">{{ workerData.status }}</span>
</template>, which means removing it now can cause it to log errors. It
is advised to shut down the Worker before removing it from the system.
</template>
</p> </p>
</section> </section>
</template> </template>

View File

@ -63,6 +63,7 @@ import SocketIOSubscriptionOperation from './model/SocketIOSubscriptionOperation
import SocketIOSubscriptionType from './model/SocketIOSubscriptionType'; import SocketIOSubscriptionType from './model/SocketIOSubscriptionType';
import SocketIOTaskLogUpdate from './model/SocketIOTaskLogUpdate'; import SocketIOTaskLogUpdate from './model/SocketIOTaskLogUpdate';
import SocketIOTaskUpdate from './model/SocketIOTaskUpdate'; import SocketIOTaskUpdate from './model/SocketIOTaskUpdate';
import SocketIOWorkerTagUpdate from './model/SocketIOWorkerTagUpdate';
import SocketIOWorkerUpdate from './model/SocketIOWorkerUpdate'; import SocketIOWorkerUpdate from './model/SocketIOWorkerUpdate';
import SubmittedJob from './model/SubmittedJob'; import SubmittedJob from './model/SubmittedJob';
import Task from './model/Task'; import Task from './model/Task';
@ -433,6 +434,12 @@ export {
*/ */
SocketIOTaskUpdate, SocketIOTaskUpdate,
/**
* The SocketIOWorkerTagUpdate model constructor.
* @property {module:model/SocketIOWorkerTagUpdate}
*/
SocketIOWorkerTagUpdate,
/** /**
* The SocketIOWorkerUpdate model constructor. * The SocketIOWorkerUpdate model constructor.
* @property {module:model/SocketIOWorkerUpdate} * @property {module:model/SocketIOWorkerUpdate}

View File

@ -54,6 +54,13 @@ export default class SocketIOSubscriptionType {
"allLastRendered" = "allLastRendered"; "allLastRendered" = "allLastRendered";
/**
* value: "allWorkerTags"
* @const
*/
"allWorkerTags" = "allWorkerTags";
/** /**
* Returns a <code>SocketIOSubscriptionType</code> enum value from a Javascript object name. * Returns a <code>SocketIOSubscriptionType</code> enum value from a Javascript object name.

View File

@ -0,0 +1,84 @@
/**
* Flamenco manager
* Render Farm manager API
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*
*/
import ApiClient from '../ApiClient';
import WorkerTag from './WorkerTag';
/**
* The SocketIOWorkerTagUpdate model module.
* @module model/SocketIOWorkerTagUpdate
* @version 0.0.0
*/
class SocketIOWorkerTagUpdate {
/**
* Constructs a new <code>SocketIOWorkerTagUpdate</code>.
* Worker Tag, sent over SocketIO when it changes.
* @alias module:model/SocketIOWorkerTagUpdate
* @param tag {module:model/WorkerTag}
*/
constructor(tag) {
SocketIOWorkerTagUpdate.initialize(this, tag);
}
/**
* Initializes the fields of this object.
* This method is used by the constructors of any subclasses, in order to implement multiple inheritance (mix-ins).
* Only for internal use.
*/
static initialize(obj, tag) {
obj['tag'] = tag;
}
/**
* Constructs a <code>SocketIOWorkerTagUpdate</code> from a plain JavaScript object, optionally creating a new instance.
* Copies all relevant properties from <code>data</code> to <code>obj</code> if supplied or a new instance if not.
* @param {Object} data The plain JavaScript object bearing properties of interest.
* @param {module:model/SocketIOWorkerTagUpdate} obj Optional instance to populate.
* @return {module:model/SocketIOWorkerTagUpdate} The populated <code>SocketIOWorkerTagUpdate</code> instance.
*/
static constructFromObject(data, obj) {
if (data) {
obj = obj || new SocketIOWorkerTagUpdate();
if (data.hasOwnProperty('tag')) {
obj['tag'] = WorkerTag.constructFromObject(data['tag']);
}
if (data.hasOwnProperty('was_deleted')) {
obj['was_deleted'] = ApiClient.convertToType(data['was_deleted'], 'Boolean');
}
}
return obj;
}
}
/**
* @member {module:model/WorkerTag} tag
*/
SocketIOWorkerTagUpdate.prototype['tag'] = undefined;
/**
* When a tag was just deleted, this is set to `true`.
* @member {Boolean} was_deleted
*/
SocketIOWorkerTagUpdate.prototype['was_deleted'] = undefined;
export default SocketIOWorkerTagUpdate;

View File

@ -7,7 +7,9 @@
</div> </div>
<footer class="app-footer"> <footer class="app-footer">
<notification-bar /> <notification-bar />
<update-listener ref="updateListener" mainSubscription="allWorkers" @workerUpdate="onSIOWorkerUpdate" <update-listener ref="updateListener"
mainSubscription="allWorkers" extraSubscription="allWorkerTags"
@workerUpdate="onSIOWorkerUpdate" @workerTagUpdate="onSIOWorkerTagsUpdate"
@sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" /> @sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
</footer> </footer>
</template> </template>
@ -85,6 +87,10 @@ export default {
this._fetchWorker(this.workerID); this._fetchWorker(this.workerID);
}, },
onSIOWorkerTagsUpdate(workerTagsUpdate) {
this.workers.refreshTags()
.then(() => this._fetchWorker(this.workerID));
},
onTableWorkerClicked(rowData) { onTableWorkerClicked(rowData) {
if (rowData.id == this.workerID) return; if (rowData.id == this.workerID) return;

View File

@ -61,6 +61,18 @@ file][workercfg].
[workercfg]: {{< ref "usage/worker-configuration" >}} [workercfg]: {{< ref "usage/worker-configuration" >}}
## My Worker cannot find Blender, what do I do?
When installing and starting the Flamenco Worker you may see a warning in the logs that says
the Worker cannot find Blender.
```
WRN Blender could not be found. Flamenco Manager will have to supply the full path to Blender when Tasks are sent to this Worker. For more help see https://flamenco.blender.org/usage/variables/blender/
```
If Flamenco cannot locate Blender on the system it is possible to use a [two-way variable named `blender`][blendervar] for each platform (eg: Windows, Linux, or MacOS). This path to Blender is then sent to the Worker for each render task. Note that the Worker will still show the warning at startup, as it cannot find Blender by itself; this is fine, because you now have configured the Manager to provide this path.
[blendervar]: {{< ref "usage/variables/blender" >}}
## Can I change the paths/names of the rendered files? ## Can I change the paths/names of the rendered files?