#!/usr/bin/env python
##############################################################################
##
# This file is part of Sardana
##
# http://www.sardana-controls.org/
##
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
##
# Sardana is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
##
# Sardana is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
##
# You should have received a copy of the GNU Lesser General Public License
# along with Sardana. If not, see <http://www.gnu.org/licenses/>.
##
##############################################################################
"""The MacroServer tango module"""
import os.path
from getpass import getuser
import taurus
from PyTango import (
READ,
READ_WRITE,
SCALAR,
SPECTRUM,
DebugIt,
DevEncoded,
DevLong,
DevState,
DevString,
DevVarStringArray,
DevVoid,
Except,
Util,
)
from taurus.core.util.codecs import CodecFactory
from sardana import SardanaServer, State
from sardana.macroserver.macroserver import MacroServer as MS
from sardana.macroserver.msexception import MacroServerException
from sardana.tango.core.SardanaDevice import SardanaDevice, SardanaDeviceClass
[docs]
class MacroServer(SardanaDevice):
"""The MacroServer tango class"""
ElementsCache = None
EnvironmentCache = None
def __init__(self, cl, name):
self._macro_server = None
SardanaDevice.__init__(self, cl, name)
[docs]
def init(self, name):
SardanaDevice.init(self, name)
if self._alias is None:
self._alias = Util.instance().get_ds_inst_name()
self._macro_server = ms = MS(self.get_full_name(), self.alias)
ms.add_listener(self.on_macro_server_changed)
@property
def macro_server(self):
return self._macro_server
[docs]
def delete_device(self):
SardanaDevice.delete_device(self)
self._macro_server.clear_log_report()
# Workaround for bug #494.
factory = taurus.Factory("tango")
for attr in list(factory.tango_attrs.values()):
attr.cleanUp()
[docs]
def init_device(self):
SardanaDevice.init_device(self)
self.set_change_event("State", True, False)
self.set_change_event("Status", True, False)
self.set_change_event("TypeList", True, False)
self.set_change_event("DoorList", True, False)
self.set_change_event("MacroList", True, False)
self.set_change_event("MacroLibList", True, False)
self.set_change_event("RecorderLibList", True, False)
self.set_change_event("RecorderList", True, False)
self.set_change_event("Elements", True, False)
self.set_change_event("Environment", True, False)
dev_class = self.get_device_class()
self.get_device_properties(dev_class)
self.EnvironmentDb = self._calculate_name(self.EnvironmentDb)
self.LogReportFilename = self._calculate_name(self.LogReportFilename)
macro_server = self.macro_server
macro_server.set_python_path(self.PythonPath)
macro_server.set_max_parallel_macros(self.MaxParallelMacros)
# if it is not possible to store/retrieve the environment from the
# current path then setup a new unique path and store the environment
# there forever
try:
macro_server.set_environment_db(self.EnvironmentDb)
except Exception as exc:
self.error("Failed to set environment DB to %s", self.EnvironmentDb)
self.debug("Details:", exc_info=1)
if "redis" in self.EnvironmentDb:
msg = "Aborting MacroServer start: cannot connect to Redis at {}. Exception: {}".format(
self.EnvironmentDb, exc
)
Except.throw_exception("InitError", msg, "init_device")
import tempfile
env_db = os.path.join(tempfile.mkdtemp(), MacroServerClass.DefaultEnvRelDir)
env_db = self._calculate_name(env_db)
db = Util.instance().get_database()
db.put_device_property(self.get_name(), dict(EnvironmentDb=env_db))
self.EnvironmentDb = env_db
macro_server.set_environment_db(self.EnvironmentDb)
try:
macro_server.set_log_report(self.LogReportFilename, self.LogReportFormat)
except Exception:
self.error("Failed to setup log report to %s", self.LogReportFilename)
self.debug("Details:", exc_info=1)
macro_server.set_recorder_path(self.RecorderPath)
macro_server.set_macro_path(self.MacroPath)
self.set_state(DevState.ON)
[docs]
def sardana_init_hook(self):
self.macro_server.set_pool_names(self.PoolNames)
def _calculate_name(self, name):
if name is None:
return None
util = Util.instance()
return name % {
"ds_name": util.get_ds_name().lower(),
"ds_exec_name": util.get_ds_exec_name(),
"ds_inst_name": util.get_ds_inst_name().lower(),
}
[docs]
def on_macro_server_changed(self, evt_src, evt_type, evt_value):
# during server startup and shutdown avoid processing element
# creation events
if SardanaServer.server_state != State.Running:
return
evt_name = evt_type.name.lower()
multi_attr = self.get_device_attr()
elems_attr = multi_attr.get_attr_by_name("Elements")
if evt_name == "poolelementschanged":
# force the element list cache to be rebuild next time someone reads
# the element list
self.ElementsCache = None
value = CodecFactory().getCodec("utf8_json").encode(("", evt_value))
self.set_attribute(elems_attr, value=value)
# self.push_change_event('Elements', *evt_value.value)
elif evt_name in ("elementcreated", "elementdeleted"):
# force the element list cache to be rebuild next time someone reads
# the element list
self.ElementsCache = None
elem = evt_value
value = {}
if "created" in evt_name:
key = "new"
else:
key = "del"
json_elem = elem.serialize(pool=self.pool.full_name)
value[key] = (json_elem,)
value = CodecFactory().getCodec("utf8_json").encode(("", value))
self.set_attribute(elems_attr, value=value)
# self.push_change_event('Elements', *value)
elif evt_name == "elementschanged":
# force the element list cache to be rebuild next time someone reads
# the element list
self.ElementsCache = None
ms_name = self.macro_server.full_name
new_values, changed_values, deleted_values = [], [], []
for elem in evt_value["new"]:
json_elem = elem.serialize(macro_server=ms_name)
new_values.append(json_elem)
for elem in evt_value["change"]:
json_elem = elem.serialize(macro_server=ms_name)
changed_values.append(json_elem)
for elem in evt_value["del"]:
json_elem = elem.serialize(macro_server=ms_name)
deleted_values.append(json_elem)
value = {"new": new_values, "change": changed_values, "del": deleted_values}
value = CodecFactory().getCodec("utf8_json").encode(("", value))
self.set_attribute(elems_attr, value=value)
# self.push_change_event('Elements', *value)
elif evt_name == "environmentchanged":
self.EnvironmentCache = None
env_attr = multi_attr.get_attr_by_name("Environment")
value = CodecFactory().getCodec("pickle").encode(("", evt_value))
self.set_attribute(env_attr, value=value)
[docs]
def always_executed_hook(self):
pass
def read_attr_hardware(self, data):
pass
[docs]
def read_DoorList(self, attr):
door_names = self.macro_server.get_door_names()
attr.set_value(door_names)
[docs]
@DebugIt()
def read_MacroList(self, attr):
macro_names = self.macro_server.get_macro_names()
attr.set_value(macro_names)
[docs]
def read_MacroLibList(self, attr):
macro_lib_names = self.macro_server.get_macro_lib_names()
attr.set_value(macro_lib_names)
[docs]
def read_RecorderLibList(self, attr):
recorder_lib_names = self.macro_server.get_recorder_lib_names()
attr.set_value(recorder_lib_names)
[docs]
def read_RecorderList(self, attr):
recorder_names = self.macro_server.get_recorder_class_names()
attr.set_value(recorder_names)
[docs]
def read_TypeList(self, attr):
type_names = self.macro_server.get_data_type_names_with_asterisc()
attr.set_value(type_names)
# @DebugIt()
[docs]
def getElements(self, cache=True):
value = self.ElementsCache
if cache and value is not None:
return value
elements = self.macro_server.get_elements_info()
value = dict(new=elements)
value = CodecFactory().getCodec("utf8_json").encode(("", value))
if SardanaServer.server_state == State.Running:
# Only cache value when the server is running
# If a client is reading Elements during server startup,
# the list might be incomplete and we don't want to cache that
self.ElementsCache = value
return value
# @DebugIt()
[docs]
def read_Elements(self, attr):
fmt, data = self.getElements()
attr.set_value(fmt, data)
[docs]
def is_Elements_allowed(self, req_type):
return SardanaServer.server_state == State.Running
is_DoorList_allowed = is_MacroList_allowed = is_MacroLibList_allowed = (
is_RecorderLibList_allowed
) = is_RecorderList_allowed = is_TypeList_allowed = is_Elements_allowed
[docs]
def GetMacroInfo(self, macro_names):
"""Get macro information
Returns a list of strings containing macro information.
Each string is a JSON encoded.
Args:
macro_names (list(str)): macro(s) name(s)
Returns:
list(str): macro(s) information
"""
macro_server = self.macro_server
codec = CodecFactory().getCodec("json")
ret = []
for _, macro in list(macro_server.get_macros().items()):
if macro.name in macro_names:
ret.append(codec.encode(("", macro.serialize()))[1])
return ret
[docs]
def ReloadMacro(self, macro_names):
"""ReloadMacro(list<string> macro_names):"""
try:
for macro_name in macro_names:
self.macro_server.reload_macro(macro_name)
except MacroServerException as mse:
Except.throw_exception(mse.type, mse.msg, "ReloadMacro")
return ["OK"]
[docs]
def ReloadMacroLib(self, lib_names):
"""ReloadMacroLib(sequence<string> lib_names):"""
try:
for lib_name in lib_names:
self.macro_server.reload_macro_lib(lib_name)
except MacroServerException as mse:
Except.throw_exception(mse.type, mse.msg, "ReloadMacroLib")
return ["OK"]
[docs]
def GetMacroCode(self, argin):
"""GetMacroCode(<module name> [, <macro name>]) -> full filename, code, line_nb"""
ret = self.macro_server.get_or_create_macro_lib(*argin)
return list(map(str, ret))
[docs]
def SetMacroCode(self, argin):
lib_name, code = argin[:2]
auto_reload = True
if len(argin) > 2:
auto_reload = argin[2].lower() in ("true", "yes")
self.macro_server.set_macro_lib(lib_name, code, auto_reload=auto_reload)
# @DebugIt()
[docs]
def getEnvironment(self, cache=True):
value = self.EnvironmentCache
if cache and value is not None:
return value
env = self.macro_server.get_env()
value = dict(new=env)
value = CodecFactory().getCodec("pickle").encode(("", value))
self.EnvironmentCache = value
return value
[docs]
def read_Environment(self, attr):
fmt, data = self.getEnvironment()
attr.set_value(fmt, data)
[docs]
def write_Environment(self, attr):
data = attr.get_write_value()
data = CodecFactory().getCodec("pickle").decode(data)[1]
self.macro_server.change_env(data)
[docs]
def is_Environment_allowed(self, req_type):
return True
[docs]
class MacroServerClass(SardanaDeviceClass):
"""MacroServer Tango class class"""
# Class Properties
class_property_list = {}
DefaultEnvBaseDir = "/tmp/tango-%s" % getuser()
DefaultEnvRelDir = "%(ds_exec_name)s/%(ds_inst_name)s/macroserver.properties"
DefaultLogReportFormat = "%(levelname)-8s %(asctime)s: %(message)s"
# Device Properties
device_property_list = {
"PoolNames": [DevVarStringArray, "Sardana device pool device names", []],
"MacroPath": [
DevVarStringArray,
"list of directories to search for macros (path separators "
"can be '\n' or character conventionally used by the OS to"
"separate search path components, such as ':' for POSIX"
"or ';' for Windows)",
[],
],
"RecorderPath": [
DevVarStringArray,
"list of directories to search for recorders (path separators "
"can be '\n' or character conventionally used by the OS to"
"separate search path components, such as ':' for POSIX"
"or ';' for Windows)",
[],
],
"PythonPath": [
DevVarStringArray,
"list of directories to be appended to sys.path at startup (path "
"separators can be '\n' or ':')",
[],
],
"MaxParallelMacros": [
DevLong,
"Maximum number of macros that can execute concurrently.",
[10],
],
"EnvironmentDb": [
DevString,
"The environment database (usually a plain file).",
os.path.join(DefaultEnvBaseDir, DefaultEnvRelDir),
],
"LogReportFilename": [
DevString,
"Filename (absolute) which contains user log reports [default: "
"None, meaning don't store log report messages]. The system will "
"save old log files by appending extensions to the filename. The "
"extensions are date-and-time based, using the strftime "
"format %Y-%m-%d_%H-%M-%S or a leading portion thereof, "
"depending on the rollover interval.",
None,
],
"LogReportFormat": [
DevString,
"Log report format [default: '%s']" % DefaultLogReportFormat,
DefaultLogReportFormat,
],
"LogstashHost": [
DevString,
"Hostname where Logstash runs. "
"This property has been included in Sardana on a provisional "
"basis. Backwards incompatible changes (up to and including "
"its removal) may occur if deemed necessary by the "
"core developers.",
None,
],
"LogstashPort": [
DevLong,
"Port on which Logstash will listen on events [default: 12345]. "
"This property has been included in Sardana on a provisional "
"basis. Backwards incompatible changes (up to and including "
"its removal) may occur if deemed necessary by the "
"core developers.",
12345,
],
"LogstashCacheDbPath": [
DevString,
"Path to the Logstash cache database [default: None]. "
"It is advised not to use the database cache, as it may "
"have negative effects on logging performance. See #895. "
"This property has been included in Sardana on a provisional "
"basis. Backwards incompatible changes (up to and including "
"its removal) may occur if deemed necessary by the "
"core developers.",
None,
],
}
# Command definitions
cmd_list = {
"GetMacroInfo": [
[DevVarStringArray, "Macro(s) name(s)"],
[DevVarStringArray, "Macro(s) description(s)"],
],
"ReloadMacro": [
[DevVarStringArray, "Macro(s) name(s)"],
[
DevVarStringArray,
"[OK] if successfull or a traceback "
"if there was a error (one string with complete traceback of "
"each error)",
],
],
"ReloadMacroLib": [
[DevVarStringArray, "MacroLib(s) name(s)"],
[
DevVarStringArray,
"[OK] if successfull or a traceback "
"if there was a error (one string with complete traceback of "
"each error)",
],
],
"GetMacroCode": [
[DevVarStringArray, "<MacroLib name> [, <Macro name>]"],
[
DevVarStringArray,
"result is a sequence of 3 strings:\n"
"<full path and file name>, <code>, <line number>",
],
],
"SetMacroCode": [
[
DevVarStringArray,
"<MacroLib name>, <code> [, <Auto reload>=True]\n"
"- if macro lib is a simple module name:\n"
" - if it exists, it is overwritten, otherwise a new python "
"file is created in the directory of the first element in "
"the MacroPath property"
"- if macro lib is the full path name:\n"
" - if path is not in the MacroPath, an exception is thrown"
" - if file exists it is overwritten otherwise a new file "
"is created",
],
[DevVoid, ""],
],
}
# Attribute definitions
attr_list = {
"DoorList": [[DevString, SPECTRUM, READ, 256]],
"MacroList": [[DevString, SPECTRUM, READ, 4096]],
"MacroLibList": [[DevString, SPECTRUM, READ, 1024]],
"RecorderLibList": [[DevString, SPECTRUM, READ, 1024]],
"RecorderList": [[DevString, SPECTRUM, READ, 1024]],
"TypeList": [[DevString, SPECTRUM, READ, 256]],
"Elements": [
[DevEncoded, SCALAR, READ],
{
"label": "Elements",
"description": "the list of all elements (a JSON encoded dict)",
},
],
"Environment": [
[DevEncoded, SCALAR, READ_WRITE],
{
"label": "Environment",
"description": "The macro server environment (a JSON encoded dict)",
},
],
}