Skip to content

Commit

Permalink
#218 added possibility to show preload script before any execution
Browse files Browse the repository at this point in the history
  • Loading branch information
bugy committed Mar 10, 2023
1 parent 035b3e6 commit 04a8201
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 9 deletions.
6 changes: 5 additions & 1 deletion samples/configs/destroy_world.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
"script_path": "/usr/bin/python3 ./samples/scripts/destroy_world.py",
"description": "This is a very dangerous script, please be careful when running. Don't forget your protective helmet.",
"requires_terminal": false,
"output_format": "terminal"
"output_format": "text",
"preload_script": {
"script": "echo 'This is a <b>preload</b> script showing some info.</br>'",
"output_format": "html"
}
}
14 changes: 14 additions & 0 deletions src/concurrency/threading_decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import functools
import threading


def threaded(func):
"""Decorator to automatically launch a function in a thread"""

@functools.wraps(func)
def wrapper(*args, **kwargs):
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
thread.start()
return thread

return wrapper
41 changes: 40 additions & 1 deletion src/model/script_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ def create_failed_short_config(path, has_admin_rights):
'output_format',
'output_files',
'schedulable',
'_included_config')
'preload_script',
'_included_config',
)
class ConfigModel:

def __init__(self,
Expand Down Expand Up @@ -213,6 +215,8 @@ def _reload_config(self):

self.logging_config = LoggingConfig.from_json(config.get('logging'))

self.preload_script = self._read_preload_script_conf(config.get('preload_script'))

if not self.script_command:
raise Exception('No script_path is specified for ' + self.name)

Expand Down Expand Up @@ -285,6 +289,41 @@ def _path_to_json(self, path):
LOGGER.warning('Failed to load included file, path does not exist: ' + path)
return None

def _read_preload_script_conf(self, config):
if config is None:
return None

error_message = 'Failed to load preload script for ' + self.name + ': '

if not isinstance(config, dict):
logging.warning(error_message + 'should be dict')
return None

script = config.get('script')
if is_empty(script):
logging.warning(error_message + 'missing "script" field')
return

try:
format = read_output_format(config)
except InvalidConfigException:
LOGGER.warning(error_message + 'invalid format specified')
format = OUTPUT_FORMAT_TERMINAL

return {'script': script, 'output_format': format}

def run_preload_script(self):
if not self.preload_script:
raise Exception('Cannot run preload script for ' + self.name + ': no preload_script is specified')

return self._process_invoker.invoke(self.preload_script.get('script'))

def get_preload_script_format(self):
if not self.preload_script:
return OUTPUT_FORMAT_TERMINAL

return self.preload_script.get('output_format')


def _read_name(file_path, json_object):
name = json_object.get('name')
Expand Down
57 changes: 57 additions & 0 deletions src/tests/script_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from tests import test_utils
from tests.test_utils import create_script_param_config, create_parameter_model, create_files
from utils import file_utils, custom_json
from utils.process_utils import ExecutionException

DEF_AUDIT_NAME = '127.0.0.1'
DEF_USERNAME = 'user1'
Expand Down Expand Up @@ -954,6 +955,62 @@ def tearDown(self) -> None:
test_utils.cleanup()


class PreloadScriptTest(unittest.TestCase):
@parameterized.expand([
({'script': 'echo 123'}, 'echo 123', 'terminal'),
({'script': 'echo 123', 'output_format': 'html'}, 'echo 123', 'html'),
({'script': 'echo 123', 'output_format': 'weird'}, 'echo 123', 'terminal'),
({'script': 'echo 123', 'unknown_field': 'html'}, 'echo 123', 'terminal'),
])
def test_load_config(self, configured_config, expected_script, expected_format):
config = _create_config_model('some_name', config={
'preload_script': configured_config
})

self.assertEqual(
config.preload_script,
{'script': expected_script, 'output_format': expected_format}
)

@parameterized.expand([
({'preload_script': None},),
({},),
({'preload_script': {}},),
({'preload_script': {'some_field': 'echo 123'}},)
])
def test_load_config_when_none(self, configured_config):
config = _create_config_model('some_name', config={
'preload_script': configured_config
})

self.assertEqual(config.preload_script, None)

def test_run_preload_script(self):
config = _create_config_model('some_name', config={
'preload_script': {'script': 'echo 123'}
})

output = config.run_preload_script()
self.assertEqual('123\n', output)

def test_run_preload_script_when_not_configured(self):
config = _create_config_model('some_name', config={
'preload_script': None
})

self.assertRaisesRegex(Exception, '.+no preload_script is specified', config.run_preload_script)

def test_run_preload_script_when_script_fails(self):
config = _create_config_model('some_name', config={
'preload_script': {'script': 'bash -c "exit -1"'}
})

self.assertRaises(ExecutionException, config.run_preload_script)

def tearDown(self) -> None:
test_utils.cleanup()


def _create_config_model(name, *,
config=None,
username=DEF_USERNAME,
Expand Down
41 changes: 39 additions & 2 deletions src/tests/web/script_config_socket_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ def test_initial_config_when_init_with_values(self):
@testing.gen_test
def test_reload_model(self):
self.socket = yield self._connect('Test script 1')
_ = yield self.socket.read_message()

message1 = yield self.socket.read_message()
self._assert_message_type(message1, 'initialConfig')

message2 = yield self.socket.read_message()
self._assert_message_type(message2, 'preloadScript')

self.socket.write_message(json.dumps({
'event': 'reloadModelValues',
Expand All @@ -61,7 +66,12 @@ def test_reload_model(self):
@testing.gen_test
def test_client_version(self):
self.socket = yield self._connect('Test script 1')
_ = yield self.socket.read_message()

message1 = yield self.socket.read_message()
self._assert_message_type(message1, 'initialConfig')

message2 = yield self.socket.read_message()
self._assert_message_type(message2, 'preloadScript')

self.socket.write_message(json.dumps({
'event': 'parameterValue',
Expand Down Expand Up @@ -121,6 +131,25 @@ def test_client_version(self):
external_model_id='abcd',
client_version=7)

@testing.gen_test
def test_preload_script(self):
self.socket = yield self._connect('Test script 1')

message1 = yield self.socket.read_message()
self._assert_message_type(message1, 'initialConfig')

message2 = yield self.socket.read_message()
event = json.loads(message2)

self.assertEqual(
{'event': 'preloadScript',
'data': {
'clientStateVersion': None,
'output': '123\n',
'format': 'terminal'
}},
event)

def assert_model(self, response, event_type, external_model_id=None, list2_values=None,
client_version=None):
event = json.loads(response)
Expand Down Expand Up @@ -181,6 +210,11 @@ def _assert_version_beat(self, response, client_version):
{'data': {'clientStateVersion': client_version}, 'event': 'clientStateVersionAccepted'},
event)

def _assert_message_type(self, message, expected_type):
event = json.loads(message)

self.assertEqual(event.get('event'), expected_type)

def _connect(self, script_name, init_with_values=False):
url = 'ws://localhost:{}/scripts/{}'.format(self.port, quote(script_name))
if init_with_values:
Expand Down Expand Up @@ -218,6 +252,9 @@ def setUp(self):
{'name': 'Test script 1',
'script_path': 'ls',
'include': '${text 1}.json',
'preload_script': {
'script': 'echo 123'
},
'parameters': [
test_utils.create_script_param_config('text 1', required=True),
test_utils.create_script_param_config('list 1', type='list',
Expand Down
18 changes: 18 additions & 0 deletions src/web/script_config_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
from tornado import gen

from auth.user import User
from concurrency.threading_decorators import threaded
from config.config_service import ConfigNotAllowedException, CorruptConfigFileException
from model import external_model
from model.external_model import parameter_to_external
from model.model_helper import read_bool
from model.script_config import ConfigModel
from utils.process_utils import ExecutionException
from web.web_auth_utils import check_authorization
from web.web_utils import wrap_to_server_event, inject_user

Expand Down Expand Up @@ -216,6 +218,22 @@ def _prepare_and_send_model(self, *, parameter_values=None, external_id=None, ev
new_config = external_model.config_to_external(config_model, self.config_id, external_id)
self.safe_write(self._create_event(event_type, new_config))

config_model.preload_script_prop.subscribe(self._send_preload_script)
self._send_preload_script(None, None)

def _create_event(self, event_type, data):
data['clientStateVersion'] = self._latest_client_state_version
return wrap_to_server_event(event_type=event_type, data=data)

@threaded
def _send_preload_script(self, _, __):
if not self.config_model.preload_script:
return

try:
text = self.config_model.run_preload_script()
format = self.config_model.get_preload_script_format()

self.safe_write(self._create_event('preloadScript', {'output': text, 'format': format}))
except ExecutionException:
LOGGER.exception('Failed to execute preload script for ' + self.config_model.name)
11 changes: 11 additions & 0 deletions web-src/src/main-app/components/scripts/script-view.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
<ScheduleButton v-if="schedulable" :disabled="!enableScheduleButton" @click="openSchedule"/>
</div>
<LogPanel v-show="showLog && !hasErrors && !hideExecutionControls" ref="logPanel" :outputFormat="outputFormat"/>
<LogPanel v-show="preloadOutput && !showLog && !hasErrors && !hideExecutionControls"
ref="preloadOutputPanel"
:output-format="preloadOutputFormat"/>
<div v-if="hasErrors" v-show="!hideExecutionControls" class="validation-panel">
<h6 class="header">Validation failed. Errors list:</h6>
<ul class="validation-errors-list">
Expand Down Expand Up @@ -102,6 +105,8 @@ export default {
loading: 'loading',
scriptConfig: 'scriptConfig',
outputFormat: state => state.scriptConfig ? state.scriptConfig.outputFormat : undefined,
preloadOutput: state => state.preloadScript?.['output'],
preloadOutputFormat: state => state.preloadScript?.['format']
}),
...mapState('scriptSetup', {
parameterErrors: 'errors'
Expand Down Expand Up @@ -377,6 +382,12 @@ export default {
}
},
preloadOutput: {
handler(newValue, _) {
this.$refs.preloadOutputPanel.setLog(newValue)
}
},
inlineImages: {
handler(newValue, oldValue) {
const logPanel = this.$refs.logPanel;
Expand Down
22 changes: 17 additions & 5 deletions web-src/src/main-app/store/scriptConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export default () => ({
parameters: [],
sentValues: {},
loading: false,
clientStateVersion: 0
clientStateVersion: 0,
preloadScript: null
},
namespaced: true,
actions: {
Expand Down Expand Up @@ -124,7 +125,7 @@ export default () => ({
}

forEachKeyValue(state.sentValues, (key, value) => sendParameterValue(key, value, websocket));
},
}
},
mutations: {
RESET_CONFIG(state) {
Expand All @@ -135,6 +136,7 @@ export default () => ({
state.loadError = null;
state.loading = false;
state.sentValues = {};
state.preloadScript = null
},

SET_ERROR(state, error) {
Expand Down Expand Up @@ -268,6 +270,10 @@ export default () => ({
break
}
}
},

SET_PRELOAD_SCRIPT(state, preloadScript) {
state.preloadScript = preloadScript
}
}
})
Expand Down Expand Up @@ -327,6 +333,12 @@ function reconnect(state, internalState, commit, dispatch, selectedScript) {

if (eventType === 'parameterRemoved') {
commit('REMOVE_PARAMETER', data);
return;
}

if (eventType === 'preloadScript') {
commit('SET_PRELOAD_SCRIPT', data)

}
},

Expand All @@ -337,11 +349,11 @@ function reconnect(state, internalState, commit, dispatch, selectedScript) {

if (error.code === 422) {
commit('SET_ERROR', `${error.reason} "${selectedScript}"`);
return;
return;
}

console.log('Socket closed. code=' + error.code + ', reason=' + error.reason);

if (isNull(state.scriptConfig)) {
commit('SET_ERROR', 'Failed to connect to the server');
return;
Expand Down
Loading

0 comments on commit 04a8201

Please sign in to comment.