Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server side plugins #448

Draft
wants to merge 6 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "client",
"version": "0.6.1",
"version": "0.6.2",
"private": true,
"scripts": {
"build": "vite build",
Expand Down
89 changes: 86 additions & 3 deletions client/src/vue_components/show/config/cues/CueEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
fluid
>
<b-row class="script-row">
<b-col cols="2" />
<b-col cols="2">
<b-button
v-b-modal.go-to-page
variant="success"
>
Go to Page
</b-button>
</b-col>
<b-col
cols="2"
style="text-align: right"
Expand Down Expand Up @@ -86,6 +93,40 @@
/>
</div>
</b-modal>
<b-modal
id="go-to-page"
ref="go-to-page"
title="Go to Page"
size="sm"
:hide-header-close="changingPage"
:hide-footer="changingPage"
:no-close-on-backdrop="changingPage"
:no-close-on-esc="changingPage"
@ok="goToPage"
>
<b-form @submit.stop.prevent="">
<b-form-group
id="page-input-group"
label="Page"
label-for="page-input"
label-cols="auto"
>
<b-form-input
id="page-input"
v-model="$v.pageInputFormState.pageNo.$model"
name="page-input"
type="number"
:state="validatePageState('pageNo')"
aria-describedby="page-feedback"
/>
<b-form-invalid-feedback
id="page-feedback"
>
This is a required field, and must be greater than 0.
</b-form-invalid-feedback>
</b-form-group>
</b-form>
</b-modal>
</b-container>
</template>

Expand All @@ -95,6 +136,8 @@ import log from 'loglevel';

import { makeURL } from '@/js/utils';
import ScriptLineCueEditor from '@/vue_components/show/config/cues/ScriptLineCueEditor.vue';
import { minValue, required } from 'vuelidate/lib/validators';
import { notNull, notNullAndGreaterThanZero } from '@/js/customValidators';

export default {
name: 'CueEditor',
Expand All @@ -115,8 +158,22 @@ export default {
savingInProgress: false,
saveError: false,
currentMaxPage: 1,
changingPage: false,
pageInputFormState: {
pageNo: 1,
},
};
},
validations: {
pageInputFormState: {
pageNo: {
required,
notNull,
notNullAndGreaterThanZero,
minValue: minValue(1),
},
},
},
async beforeMount() {
// Config status
await this.GET_SCRIPT_CONFIG_STATUS();
Expand All @@ -133,8 +190,12 @@ export default {
await this.getMaxScriptPage();

// Initialisation of page data
await this.LOAD_SCRIPT_PAGE(this.currentEditPage);
await this.LOAD_SCRIPT_PAGE(this.currentEditPage + 1);
// Initialisation of page data
const storedPage = localStorage.getItem('cueEditPage');
if (storedPage != null) {
this.currentEditPage = parseInt(storedPage, 10);
}
await this.goToPageInner(this.currentEditPage);
},
methods: {
async getMaxScriptPage() {
Expand Down Expand Up @@ -179,6 +240,23 @@ export default {
}
return [];
},
validatePageState(name) {
const { $dirty, $error } = this.$v.pageInputFormState[name];
return $dirty ? !$error : null;
},
async goToPage() {
this.changingPage = true;
await this.goToPageInner(this.pageInputFormState.pageNo);
this.changingPage = false;
},
async goToPageInner(pageNo) {
if (pageNo > 1) {
await this.LOAD_SCRIPT_PAGE(parseInt(pageNo, 10) - 1);
}
await this.LOAD_SCRIPT_PAGE(pageNo);
this.currentEditPage = pageNo;
await this.LOAD_SCRIPT_PAGE(parseInt(pageNo, 10) + 1);
},
...mapMutations(['REMOVE_PAGE', 'ADD_BLANK_LINE', 'SET_LINE']),
...mapActions(['GET_SCENE_LIST', 'GET_ACT_LIST', 'GET_CHARACTER_LIST',
'GET_CHARACTER_GROUP_LIST', 'LOAD_SCRIPT_PAGE', 'ADD_BLANK_PAGE', 'GET_SCRIPT_CONFIG_STATUS',
Expand All @@ -199,5 +277,10 @@ export default {
'CHARACTER_GROUP_LIST', 'CAN_REQUEST_EDIT', 'CURRENT_EDITOR', 'INTERNAL_UUID',
'GET_SCRIPT_PAGE', 'DEBUG_MODE_ENABLED', 'CUE_TYPES', 'SCRIPT_CUES', 'SCRIPT_CUTS']),
},
watch: {
currentEditPage(val) {
localStorage.setItem('cueEditPage', val);
},
},
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
:no-close-on-esc="changingPage"
@ok="goToPage"
>
<b-form>
<b-form @submit.stop.prevent="">
<b-form-group
id="page-input-group"
label="Page"
Expand Down
3 changes: 2 additions & 1 deletion server/controllers/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ def get(self):
self.set_header('Content-Type', 'application/json')
self.write({
'status': 'OK',
'imported_controllers': list(IMPORTED_CONTROLLERS)
'imported_controllers': list(IMPORTED_CONTROLLERS),
'imported_plugins': list(self.application.plugin_manager.IMPORTED_PLUGINS)
})


Expand Down
38 changes: 37 additions & 1 deletion server/digi_server/app_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os
from typing import List, Optional
import socket
from typing import List, Optional, Any

import tornado
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.web import Application, StaticFileHandler
from tornado_prometheus import PrometheusMixIn
Expand All @@ -18,12 +21,14 @@
from rbac.rbac import RBACController
from utils.database import DigiSQLAlchemy
from utils.env_parser import EnvParser
from digi_server.plugin_manager import PluginManager
from utils.web.route import Route


class DigiScriptServer(PrometheusMixIn, Application):

def __init__(self, debug=False, settings_path=None):
self.http_server: Optional[HTTPServer] = None
self.env_parser: EnvParser = EnvParser.instance() # pylint: disable=no-member

self.digi_settings: Settings = Settings(self, settings_path)
Expand All @@ -33,6 +38,10 @@ def __init__(self, debug=False, settings_path=None):
# Controller imports (needed to trigger the decorator)
controllers.import_all_controllers()

# Plugin imports (needed to trigger decorator)
self.plugin_manager: PluginManager = PluginManager(self)
self.plugin_manager.import_all_plugins()

self.clients: List[WebSocketController] = []

db_path = self.digi_settings.settings.get('db_path').get_value()
Expand All @@ -51,6 +60,13 @@ def __init__(self, debug=False, settings_path=None):
session.query(Session).delete()
session.commit()

# Check for presence of admin user, and update settings to match
with self._db.sessionmaker() as session:
any_admin = session.query(User).filter(User.is_admin).first()
has_admin = any_admin is not None
self.digi_settings.settings['has_admin_user'].set_value(has_admin, False)
self.digi_settings._save()

# Check the show we are expecting to be loaded exists
with self._db.sessionmaker() as session:
current_show = self.digi_settings.settings.get('current_show').get_value()
Expand Down Expand Up @@ -94,9 +110,29 @@ def __init__(self, debug=False, settings_path=None):
login_url='/login',
)

def listen(self,
port: int,
address: Optional[str] = None,
*,
family: socket.AddressFamily = socket.AF_UNSPEC,
backlog: int = tornado.netutil._DEFAULT_BACKLOG,
flags: Optional[int] = None,
reuse_port: bool = False,
**kwargs: Any) -> HTTPServer:
self.http_server = super().listen(port, address, family=family, backlog=backlog, flags=flags,
reuse_port=reuse_port, **kwargs)
return self.http_server

async def configure(self):
await self._configure_logging()

async def post_configure(self):
await self.plugin_manager.start_default()

async def shutdown(self):
get_logger().warning('DigiScript shutting down - goodbye!')
await self.plugin_manager.shutdown()

async def _configure_logging(self):
get_logger().info('Reconfiguring logging!')

Expand Down
39 changes: 39 additions & 0 deletions server/digi_server/plugin_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import importlib
from typing import List, TYPE_CHECKING

from digi_server.logger import get_logger
from utils.pkg_utils import find_end_modules
from utils.server_plugin import BasePlugin, registered_plugins

if TYPE_CHECKING:
from digi_server.app_server import DigiScriptServer


class PluginManager:
def __init__(self, application: 'DigiScriptServer'):
self.application = application
self.IMPORTED_PLUGINS = {}
self.RUNNING_PLUGINS: List[BasePlugin] = []

def import_all_plugins(self):
plugins = find_end_modules('.', prefix='server_plugins')
for plugin in plugins:
if plugin != __name__:
get_logger().debug(f'Importing plugin module {plugin}')
mod = importlib.import_module(plugin)
self.IMPORTED_PLUGINS[plugin] = mod

async def start_plugin(self, name):
get_logger().info(f'Starting plugin: {name}')
plugin_obj = registered_plugins[name](self.application)
await plugin_obj.start()
self.RUNNING_PLUGINS.append(plugin_obj)

async def start_default(self):
for plugin_name in registered_plugins:
if registered_plugins[plugin_name]._default_plugin:
await self.start_plugin(plugin_name)

async def shutdown(self):
for plugin in self.RUNNING_PLUGINS:
await plugin.stop()
4 changes: 4 additions & 0 deletions server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ async def main():
get_logger().info(f'Listening on port: {options.port}')
if options.debug:
get_logger().warning('Running in debug mode')
await app.post_configure()

await asyncio.Event().wait()

await app.shutdown()


if __name__ == '__main__':
asyncio.run(main())
1 change: 1 addition & 0 deletions server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ marshmallow-sqlalchemy==0.29.0
tornado-prometheus==0.1.2
bcrypt==4.0.1
anytree==2.8.0
python-osc==1.8.1
Empty file.
40 changes: 40 additions & 0 deletions server/server_plugins/osc_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import asyncio
from asyncio import DatagramProtocol
from asyncio.transports import BaseTransport
from typing import List, Tuple, Any

from pythonosc.dispatcher import Dispatcher
from pythonosc.osc_server import AsyncIOOSCUDPServer

from digi_server.logger import get_logger
from utils.server_plugin import plugin, BasePlugin


def default_osc_handler(address: str, *osc_arguments: List[Any]):
get_logger().debug(f'OSC message received with no handler. Address: {address}. '
f'Arguments: {osc_arguments}')


@plugin('OSC Server', default=True)
class OSCServerPlugin(BasePlugin):
def __init__(self, application: 'DigiScriptServer'):
self.dispatcher: Dispatcher = Dispatcher()
self.dispatcher.set_default_handler(default_osc_handler)
self.servers: List[AsyncIOOSCUDPServer] = []
self.transport_pairs: List[Tuple[BaseTransport, DatagramProtocol]] = []

super().__init__(application)

async def start(self):
for fd, socket in self.application.http_server._sockets.items():
address = socket.getsockname()
server = AsyncIOOSCUDPServer((address[0], address[1] + 1),
self.dispatcher,
asyncio.get_event_loop())
transport, protocol = await server.create_serve_endpoint()
self.servers.append(server)
self.transport_pairs.append((transport, protocol))

async def stop(self):
for transport_pair in self.transport_pairs:
transport_pair[0].close()
36 changes: 36 additions & 0 deletions server/utils/server_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from digi_server.app_server import DigiScriptServer

registered_plugins = {}


def plugin(name, default=False):
def decorator(cls):
if not issubclass(cls, BasePlugin):
raise ValueError('Class must inherit from BasePlugin.')

if name in registered_plugins:
raise ValueError('Name must be unique.')

cls._default_plugin = default
registered_plugins[name] = cls

return cls

return decorator


class BasePlugin:

def __init__(self, application: 'DigiScriptServer'):
self.application = application

async def start(self):
raise NotImplementedError

async def stop(self):
raise NotImplementedError