# SPDX-License-Identifier: BSD-3-Clause AND Apache-2.0
# Copyright 2018 Regents of the University of California
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Copyright 2019 Blue Cheetah Analog Design Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This class defines SkillOceanServer, a server that handles skill/ocean requests.
The SkillOceanServer listens for skill/ocean requests from bag. Skill commands will
be forwarded to Virtuoso for execution, and Ocean simulation requests will be handled
by starting an Ocean subprocess. It also provides utility for bag to query simulation
progress and allows parallel simulation.
Client-side communication:
the client will always send a request object, which is a python dictionary.
This script processes the request and sends the appropriate commands to
Virtuoso.
Virtuoso side communication:
To ensure this process receive all the data from Virtuoso properly, Virtuoso
will print a single line of integer indicating the number of bytes to read.
Then, virtuoso will print out exactly that many bytes of data, followed by
a newline (to flush the standard input). This script handles that protcol
and will strip the newline before sending result back to client.
"""
import traceback
import bag.io
[docs]def _object_to_skill_file_helper(py_obj, file_obj):
"""Recursive helper function for object_to_skill_file
Parameters
----------
py_obj : any
the object to convert.
file_obj : file
the file object to write to. Must be created with bag.io
package so that encodings are handled correctly.
"""
# fix potential raw bytes
py_obj = bag.io.fix_string(py_obj)
if isinstance(py_obj, str):
# string
file_obj.write(py_obj)
elif isinstance(py_obj, float):
# prepend type flag
file_obj.write('#float {:f}'.format(py_obj))
elif isinstance(py_obj, bool):
bool_val = 1 if py_obj else 0
file_obj.write('#bool {:d}'.format(bool_val))
elif isinstance(py_obj, int):
# prepend type flag
file_obj.write('#int {:d}'.format(py_obj))
elif isinstance(py_obj, list) or isinstance(py_obj, tuple):
# a list of other objects.
file_obj.write('#list\n')
for val in py_obj:
_object_to_skill_file_helper(val, file_obj)
file_obj.write('\n')
file_obj.write('#end')
elif isinstance(py_obj, dict):
# disembodied property lists
file_obj.write('#prop_list\n')
for key, val in py_obj.items():
file_obj.write('{}\n'.format(key))
_object_to_skill_file_helper(val, file_obj)
file_obj.write('\n')
file_obj.write('#end')
else:
raise Exception('Unsupported python data type: %s' % type(py_obj))
[docs]def object_to_skill_file(py_obj, file_obj):
"""Write the given python object to a file readable by Skill.
Write a Python object to file that can be parsed into equivalent
skill object by Virtuoso. Currently only strings, lists, and dictionaries
are supported.
Parameters
----------
py_obj : any
the object to convert.
file_obj : file
the file object to write to. Must be created with bag.io
package so that encodings are handled correctly.
"""
_object_to_skill_file_helper(py_obj, file_obj)
file_obj.write('\n')
[docs]bag_proc_prompt = 'BAG_PROMPT>>> '
[docs]class SkillServer(object):
"""A server that handles skill commands.
This server is started and ran by virtuoso. It listens for commands from bag
from a ZMQ socket, then pass the command to virtuoso. It then gather the result
and send it back to bag.
Parameters
----------
router : :class:`bag.interface.ZMQRouter`
the :class:`~bag.interface.ZMQRouter` object used for socket communication.
virt_in : file
the virtuoso input file. Must be created with bag.io
package so that encodings are handled correctly.
virt_out : file
the virtuoso output file. Must be created with bag.io
package so that encodings are handled correctly.
tmpdir : str or None
if given, will save all temporary files to this folder.
"""
def __init__(self, router, virt_in, virt_out, tmpdir=None):
"""Create a new SkillOceanServer instance.
"""
self.handler = router
self.virt_in = virt_in
self.virt_out = virt_out
# create a directory for all temporary files
self.dtmp = bag.io.make_temp_dir('skillTmp', parent_dir=tmpdir)
[docs] def run(self):
"""Starts this server.
"""
while not self.handler.is_closed():
# check if socket received message
if self.handler.poll_for_read(5):
req = self.handler.recv_obj()
if isinstance(req, dict) and 'type' in req:
if req['type'] == 'exit':
self.close()
elif req['type'] == 'skill':
expr, out_file = self.process_skill_request(req)
if expr is not None:
# send expression to virtuoso
self.send_skill(expr)
msg = self.recv_skill()
self.process_skill_result(msg, out_file)
else:
msg = '*Error* bag server error: bag request:\n%s' % str(req)
self.handler.send_obj(dict(type='error', data=msg))
else:
msg = '*Error* bag server error: bag request:\n%s' % str(req)
self.handler.send_obj(dict(type='error', data=msg))
[docs] def send_skill(self, expr):
"""Sends expr to virtuoso for evaluation.
Parameters
----------
expr : string
the skill expression.
"""
self.virt_in.write(expr)
self.virt_in.flush()
[docs] def recv_skill(self):
"""Receive response from virtuoso"""
num_bytes = int(self.virt_out.readline())
msg = self.virt_out.read(num_bytes)
if msg[-1] == '\n':
msg = msg[:-1]
return msg
[docs] def close(self):
"""Close this server."""
self.handler.close()
[docs] def process_skill_request(self, request):
"""Process the given skill request.
Based on the given request object, returns the skill expression
to be evaluated by Virtuoso. This method creates temporary
files for long input arguments and long output.
Parameters
----------
request : dict
the request object.
Returns
-------
expr : str or None
expression to be evaluated by Virtuoso. If None, an error occurred and
nothing needs to be evaluated
out_file : str or None
if not None, the result will be written to this file.
"""
try:
expr = request['expr']
input_files = request['input_files'] or {}
out_file = request['out_file']
except KeyError as e:
msg = '*Error* bag server error: %s' % str(e)
self.handler.send_obj(dict(type='error', data=msg))
return None, None
fname_dict = {}
# write input parameters to files
for key, val in input_files.items():
with bag.io.open_temp(prefix=key, delete=False, dir=self.dtmp) as file_obj:
fname_dict[key] = '"%s"' % file_obj.name
# noinspection PyBroadException
try:
object_to_skill_file(val, file_obj)
except Exception:
stack_trace = traceback.format_exc()
msg = '*Error* bag server error: \n%s' % stack_trace
self.handler.send_obj(dict(type='error', data=msg))
return None, None
# generate output file
if out_file:
with bag.io.open_temp(prefix=out_file, delete=False, dir=self.dtmp) as file_obj:
fname_dict[out_file] = '"%s"' % file_obj.name
out_file = file_obj.name
# fill in parameters to expression
expr = expr.format(**fname_dict)
return expr, out_file
[docs] def process_skill_result(self, msg, out_file=None):
"""Process the given skill output, then send result to socket.
Parameters
----------
msg : str
skill expression evaluation output.
out_file : str or None
if not None, read result from this file.
"""
# read file if needed, and only if there are no errors.
if msg.startswith('*Error*'):
# an error occurred, forward error message directly
self.handler.send_obj(dict(type='error', data=msg))
elif out_file:
# read result from file.
try:
msg = bag.io.read_file(out_file)
data = dict(type='str', data=msg)
except IOError:
stack_trace = traceback.format_exc()
msg = '*Error* error reading file:\n%s' % stack_trace
data = dict(type='error', data=msg)
self.handler.send_obj(data)
else:
# return output from virtuoso directly
self.handler.send_obj(dict(type='str', data=msg))