Пример #1
0
    def my_server(self):
        '''
        Return an OnDemandServer bus server singleton.
        '''

        if self.bus_service is not None:
            return self.bus_service

        # Create an OnDemandPubliser that will stream on
        # the standard topic/content:

        self.bus_service = OnDemandPublisher(streamMsgs=(None, None))

        # Queues into which the server will place msgs that
        # arrive on topics subscribed to through the Web UI:
        self.bus_msg_queue = self.bus_service.bus_msg_queue
        self.bus_stats_queue = self.bus_service.bus_stats_queue

        # Before starting the bus server, pause its message streaming server:
        self.bus_service.streaming = False
        self.bus_service.start()
        self.bus_service.serve_echo = True
        self.bus_service.check_syntax = True
        return self.bus_service
 def my_server(self):
     '''
     Return an OnDemandServer bus server singleton.
     '''
     
     if self.bus_service is not None:
         return self.bus_service
     
     # Create an OnDemandPubliser that will stream on
     # the standard topic/content:
      
     self.bus_service = OnDemandPublisher(streamMsgs=(None,None))
     
     # Queues into which the server will place msgs that
     # arrive on topics subscribed to through the Web UI:
     self.bus_msg_queue = self.bus_service.bus_msg_queue
     self.bus_stats_queue = self.bus_service.bus_stats_queue
     
     # Before starting the bus server, pause its message streaming server:
     self.bus_service.streaming = False
     self.bus_service.start()
     self.bus_service.serve_echo = True
     self.bus_service.check_syntax = True
     return self.bus_service
Пример #3
0
class BrowserInteractorThread(threading.Thread):
    '''
    Thread responsible for servicing requests coming in from
    the Web UI. The requests are controls for the test instrument.
    Requests are fed through a queue that is passed in to __init__()
    
    Return strings are delivered to the main thread with scheduling
    a Tornado callback to _write_to_browser().
    '''

    # How often to check whether someone called stop()
    # on this thread:
    CHECK_DONE_PERIOD = 1  # second

    def __init__(self, websocket_comm_obj, browser_request_queue):
        '''
        Start service of requests from the Web UI.
        
        :param websocket_comm_obj: instance of BusTesterWebController in the main thread.
        :type websocket_comm_obj: BusTesterWebController
        :param browser_request_queue: message queue from which requests from the Web UI
            are passed into this instance.
        :type browser_request_queue: Queue.Queue
        '''
        super(BrowserInteractorThread,
              self).__init__(name='BrowserReqServerThread')

        self.websocket_comm_obj = websocket_comm_obj
        self.browser_request_queue = browser_request_queue
        self.bus_service = None

        # Queues through which other threads provide
        # messages that arrive from the SchoolBus to this instance
        # for transfer over the websocket to the browser:

        self.bus_msg_queue = None
        self.bus_stats_queue = None

        # Callback in parent that safely writes to websocket:
        self._write_to_browser_callback = functools.partial(
            self.websocket_comm_obj._write_to_browser)

        # Create a periodic callback that checks the in-msg and in-stats
        # queues for msgs/stats to forward to the browser. The
        # instance() method enforces a singleton ioloop instance:

        self.periodic_callback = tornado.ioloop.PeriodicCallback(
            functools.partial(self.on_bus_message),
            BusTesterWebController.PERIODIC_IN_MSG_CHECK)
        self.done = False
        self.periodic_callback.start()

    def stop(self):
        '''
        Stop the Web UI request servicing, cleaning up all underlying
        threads.
        '''

        # Stop the checks for incoming messages:
        if self.periodic_callback is not None:
            self.periodic_callback.stop()

        self.done = True
        # Immediately unblock the queue
        self.websocket_comm_obj.browser_request_queue.put_nowait('\0')

        if self.server_running():
            self.my_server.stop()

    def write_to_browser(self, msg):
        '''
        Send a message to the browser on the other end
        of the websocket. You may call this method from
        any thread. It schedules the actual write to happen
        during the next ioloop iteration, which is a 
        threadsafe procedure. Always use this method to 
        write to the websocket, don't use self.write_message 
        anywhere.
        
        :param msg: text to send
        :type msg: string
        '''
        tornado.ioloop.IOLoop.instance().add_callback(
            self._write_to_browser_callback, msg)

    def run(self):

        while not self.done:

            try:
                # Wait for requests from the Web UI:
                msg_dict = self.browser_request_queue.get(
                    DO_BLOCK, BrowserInteractorThread.CHECK_DONE_PERIOD)
                # Double check: another way to release
                # the block of this queue is to feed it a '\0':
                if msg_dict == '\0':
                    continue
            except Queue.Empty:
                continue

            # Turn string values 'True', 'False', 'on', 'off' into bools:
            chkSyntax = msg_dict.get('chkSyntax', None)
            echo = msg_dict.get('echo', None)
            streaming = msg_dict.get('streaming', None)

            # Normalize the checkbox values to Python booleans:
            if type(chkSyntax) == str:
                msg_dict['chkSyntax'] = True if chkSyntax.lower(
                ) == 'true' or chkSyntax == 'on' else False
            if type(echo) == str:
                msg_dict['echo'] = True if echo.lower(
                ) == 'true' or echo == 'on' else False
            if type(streaming) == str:
                msg_dict['streaming'] = True if streaming.lower(
                ) == 'true' or streaming == 'on' else False

            # Ensure that streamInterval is a float:
            try:
                interval = msg_dict.get('streamInterval', None)
                # If empty string, indicating request for current
                # value, the call to get_or_set_server_parm() will
                # take care of it. But otherwise, floatify:
                if interval != '':
                    msg_dict['streamInterval'] = float(interval)
            except (ValueError, TypeError):
                self.return_error(
                    {},
                    "Received a non-float for streamInterval from browser: '%s'"
                    % str(interval))
                continue

            # Workhorse for servicing the requests:
            response_dict = self.service_browser_request(
                msg_dict)  #@UnusedVariable
            continue
        return

    def service_browser_request(self, msg_dict):
        '''
        Given a dict of request key/values from the Web UI,
        set parameters in the test instrument, or return the
        current parameter values, if the values from the UI
        are empty strings. The following example would
        set the topic to which 'one-shot' messages are sent
        to 'myTopic,' and the content of one-shots to a
        string. Given the empty string for strLen, the 
        return dict will contain the current instrument
        length of random strings:
        
        Example:  {'oneShotTopic'   : 'myTopic',
                   'oneShotContent' : 'my oneshot message content',
                   'strLen'         : ''
                   }
        
        :param msg_dict: parameter names and values
        :type msg_dict: {str : str}
        '''

        response_dict = {}
        try:

            # Are we to fire a one-shot message?
            one_shot_txt = msg_dict.get('oneShot', None)
            if one_shot_txt is not None:
                # If a msg was provided as the value of
                # the oneShot request, use it; if an empty
                # string was given, use the one-shot msg on
                # record:
                msg_txt = one_shot_txt if len(
                    one_shot_txt) > 0 else self.my_server['oneShotContent']

                # Same for destination topic: if supplied, use it,
                # else use default:
                topic_in_dict = msg_dict.get('oneShotTopic', None)
                msg_topic = topic_in_dict if topic_in_dict is not None else self.my_server[
                    'oneShotTopic']

                one_shot_msg = BusMessage(topicName=msg_topic, content=msg_txt)
                self.my_server.testBus.publish(one_shot_msg)
                response_dict['success'] = 'OK'
                self.write_to_browser(json.dumps(response_dict))
                return

            # Go through each server parm in the request dict,
            # and update the server to the respective value;
            # also fill the response_dict with the new value.
            # One UI optimization: do the "length of random string"
            # first, because if subsequent one-shot content or
            # message-stream content fields are to be changed,
            # They will be the correct length. Without this
            # order switch, the user needs to hit submit twice:

            if msg_dict.get('strLen', None) is not None:
                response_dict = self.get_or_set_server_parm(
                    'strLen', msg_dict['strLen'], response_dict)
                del msg_dict['strLen']

            for (parm_key, parm_value) in msg_dict.items():
                response_dict = self.get_or_set_server_parm(
                    parm_key, parm_value, response_dict)

            # Send a dict with keys being the HTML parameter
            # names, and values being the current server
            # parameter values:
            self.write_to_browser(json.dumps(response_dict))

        except ValueError:
            # Was handled in one of the functions called above:
            #******
            raise
            #******
            return response_dict
        except Exception as e:
            print('Exception in GET: %s' % ` e `)
            self.return_error(response_dict, ` e `)
            #*********
            raise
            #*********
            return response_dict

    def on_bus_message(self, msg=None):
        '''
        Called when a message to which we are subscribed
        comes in. These are msgs on topics explicitly subscribed
        to via the Web UI. If msg is None, check self.bus_msg_queue
        for newly arrived msgs from the bus:
        
        :param msg: message to write to browser.
        :type msgs: {string | None}
        '''

        if msg is None:
            # If message/stats queues have not been initialized
            # yet, just return:
            if self.bus_msg_queue is None:
                return
            # We check the msg queue for messages:
            while not self.bus_msg_queue.empty():
                msg = self.bus_msg_queue.get_nowait()
                try:
                    self.write_to_browser({"inmsg": msg + '\r\n'})
                except Exception as e:
                    self.logErr(
                        "Error during read of msg or stats queue from OnDemandServer: '%s'"
                        % ` e `)
                    return
        else:
            try:
                self.write_to_browser({"inmsg": msg + '\r\n'})
            except Exception as e:
                self.logErr(
                    "Error during read of msg or stats queue from OnDemandServer: '%s'"
                    % ` e `)
                return

        # Check for new bus statistics to forward to browser:
        self.on_bus_stats()

    def on_bus_stats(self, msg=None):
        '''
        Called when stats about messages to which we are subscribed
        come in. These are msgs on topics explicitly subscribed
        to via the Web UI. If msg is None, check self.bus_stats_queue
        for newly arrived msgs from the bus:
        
        :param msg: stats message to write to browser.
        :type msgs: {string | None}
        '''

        if msg is None:
            # We check the stats queue for messages:
            while not self.bus_stats_queue.empty():
                msg = self.bus_stats_queue.get_nowait()
                try:
                    self.write_to_browser({"instat": msg + '\r\n'})
                except Exception as e:
                    self.logErr(
                        "Error during read of msg or stats queue from OnDemandServer: '%s'"
                        % ` e `)
                    return
        else:
            try:
                self.write_to_browser({"instat": msg + '\r\n'})
            except Exception as e:
                self.logErr(
                    "Error during read of msg or stats queue from OnDemandServer: '%s'"
                    % ` e `)
                return

    def return_error(self, response_dict, error_str):
        '''
        Add entry {'error' : <error_str>} to the 
        passed-in response_dict, and cause
        that dict to be  to be written to the browser via the standing
        websocket.
        
        :param response_dict: dict with parameter name/values
            that worked before the error occurred.
        :type response_dict: {str : str}
        :param error_str: error message
        :type error_str: str
        '''
        response_dict['error'] = error_str
        self.write_to_browser(json.dumps(response_dict))

    def get_or_set_server_parm(self, parm_name, parm_val, response_dict):
        '''
        Given one parm/value pair, get or set the 
        corresponding parameter in the test instrument.
        
        :param parm_name: name of instrument parameter
        :type parm_name: str
        :param parm_val: new value or empty string, if current value is requested
        :type parm_val: str
        :param response_dict: current response dict. This method will add
            to the passed-in dict, and return the augmented dict.
        :type response_dict: {str : str}
        :return: augmented result dict destined for the browser once all
            requests have been processed.
        :rtype: {str : str}
        '''

        # Most parameters are singleton arrays.
        # Make those into non-arrays:
        if type(parm_val) == list and len(parm_val) == 1:
            parm_val = parm_val[0]

        try:
            # Is this a request for the current value of the server setting?
            # Special cases: oneShotContent and echoContent: setting
            # values of length 0 means: set to default strings of standard
            # length, so the next branch of this conditional is the
            # one to take for these cases; other zero-length values
            # indicate request for current value:
            if len(str(parm_val)) == 0 and \
                   self.my_server is not None and \
                   parm_name != 'oneShotContent' and \
                   parm_name != 'echoContent' and \
                   parm_name != 'streamContent' and \
                   parm_name != 'topicsToRx':
                # Return current value:
                response_dict[parm_name] = self.my_server[parm_name]
                return response_dict

            # We are to set the server parm:
            if self.my_server is not None:
                # Only change if different:
                if self.my_server[parm_name] != parm_val:

                    #********
                    # Because of the instability of subscribe/unsubscribe
                    # during stream publishing: pause streaming if it's
                    # on:
                    was_streaming = False
                    if parm_name != 'streaming' and self.my_server.streaming:
                        was_streaming = True
                        self.my_server.streaming = False
                    #********

                    self.my_server[parm_name] = parm_val

                    #************
                    # Turn streaming back on if it was happening:
                    if was_streaming:
                        self.my_server.streaming = True
                    #************

                    # Read value back out from the server,
                    # b/c the setters may modify values (e.g.
                    # entering defaults when strings are empty):
                    response_dict[parm_name] = self.my_server[parm_name]
                else:
                    response_dict[parm_name] = parm_val
            else:
                # It's a request for current value, but no server is running;
                # report back to the browser, and close the connection:
                self.return_error('SchoolBus server is not running.')
                # The following will be ignored in the POST method:
                raise ValueError('SchoolBus server is not running.')
        except KeyError:
            self.return_error(
                response_dict,
                "Server parameter '%s' does not exist" % parm_name)
            raise ValueError("Server parameter '%s' does not exist" %
                             parm_name)
        except ValueError as e:
            self.return_error(response_dict, ` e `)
            # Re-raising ValueError ensures that caller
            # knows we already returned an error to the
            # browser
            raise

        return response_dict

    @property
    def my_server(self):
        '''
        Return an OnDemandServer bus server singleton.
        '''

        if self.bus_service is not None:
            return self.bus_service

        # Create an OnDemandPubliser that will stream on
        # the standard topic/content:

        self.bus_service = OnDemandPublisher(streamMsgs=(None, None))

        # Queues into which the server will place msgs that
        # arrive on topics subscribed to through the Web UI:
        self.bus_msg_queue = self.bus_service.bus_msg_queue
        self.bus_stats_queue = self.bus_service.bus_stats_queue

        # Before starting the bus server, pause its message streaming server:
        self.bus_service.streaming = False
        self.bus_service.start()
        self.bus_service.serve_echo = True
        self.bus_service.check_syntax = True
        return self.bus_service

    def server_running(self):
        '''
        Return True/False for whether the underlying 
        OnDemandPublisher instance, i.e. the test instrument
        is running.
        :return: test instrument running status
        :rtype: bool
        '''
        try:
            return self.bus_service is not None and self.bus_service.running
        except Exception:
            return False

    def ensure_lower_non_array(self, val):
        '''
        Given a string, or array of strings, return
        an array (possibly containing just the passed-in
        string), in which all strings are lower case:
        
        :param val: input value
        :type val: {str | [str]}
        '''
        if type(val) == list:
            new_val = val[0].lower()
        else:
            new_val = val.lower()
        return new_val

    def logInfo(self, msg):
        self.websocket_comm_obj.logInfo(msg)

    def logErr(self, msg):
        self.websocket_comm_obj.logErr(msg)

    def logDebug(self, msg):
        self.websocket_comm_obj.logDebug(msg)
class BrowserInteractorThread(threading.Thread):
    '''
    Thread responsible for servicing requests coming in from
    the Web UI. The requests are controls for the test instrument.
    Requests are fed through a queue that is passed in to __init__()
    
    Return strings are delivered to the main thread with scheduling
    a Tornado callback to _write_to_browser().
    '''

    # How often to check whether someone called stop()
    # on this thread:    
    CHECK_DONE_PERIOD = 1 # second
    
    def __init__(self, websocket_comm_obj, browser_request_queue):
        '''
        Start service of requests from the Web UI.
        
        :param websocket_comm_obj: instance of BusTesterWebController in the main thread.
        :type websocket_comm_obj: BusTesterWebController
        :param browser_request_queue: message queue from which requests from the Web UI
            are passed into this instance.
        :type browser_request_queue: Queue.Queue
        '''
        super(BrowserInteractorThread, self).__init__(name='BrowserReqServerThread')
        
        self.websocket_comm_obj = websocket_comm_obj
        self.browser_request_queue = browser_request_queue
        self.bus_service = None

        # Queues through which other threads provide
        # messages that arrive from the SchoolBus to this instance
        # for transfer over the websocket to the browser:
        
        self.bus_msg_queue = None
        self.bus_stats_queue = None
        
        # Callback in parent that safely writes to websocket:
        self._write_to_browser_callback = functools.partial(self.websocket_comm_obj._write_to_browser)
        
        # Create a periodic callback that checks the in-msg and in-stats
        # queues for msgs/stats to forward to the browser. The
        # instance() method enforces a singleton ioloop instance:
        
        self.periodic_callback = tornado.ioloop.PeriodicCallback(functools.partial(self.on_bus_message),
                                                                 BusTesterWebController.PERIODIC_IN_MSG_CHECK

                                                                 )
        self.done = False
        self.periodic_callback.start()
        
    def stop(self):
        '''
        Stop the Web UI request servicing, cleaning up all underlying
        threads.
        '''

        # Stop the checks for incoming messages:
        if self.periodic_callback is not None:
            self.periodic_callback.stop()
            
        self.done = True
        # Immediately unblock the queue
        self.websocket_comm_obj.browser_request_queue.put_nowait('\0')
        
        if self.server_running():
            self.my_server.stop()

    def write_to_browser(self, msg):
        '''
        Send a message to the browser on the other end
        of the websocket. You may call this method from
        any thread. It schedules the actual write to happen
        during the next ioloop iteration, which is a 
        threadsafe procedure. Always use this method to 
        write to the websocket, don't use self.write_message 
        anywhere.
        
        :param msg: text to send
        :type msg: string
        '''
        tornado.ioloop.IOLoop.instance().add_callback(self._write_to_browser_callback, msg)

    def run(self):
        
        while not self.done:
            
            try:
                # Wait for requests from the Web UI:
                msg_dict = self.browser_request_queue.get(DO_BLOCK, BrowserInteractorThread.CHECK_DONE_PERIOD)
                # Double check: another way to release
                # the block of this queue is to feed it a '\0':
                if msg_dict == '\0':
                    continue
            except Queue.Empty:
                continue 
        
            # Turn string values 'True', 'False', 'on', 'off' into bools:
            chkSyntax = msg_dict.get('chkSyntax', None)
            echo      = msg_dict.get('echo', None)
            streaming = msg_dict.get('streaming', None)
            
            # Normalize the checkbox values to Python booleans:
            if type(chkSyntax) == str:
                msg_dict['chkSyntax'] = True if chkSyntax.lower() == 'true' or chkSyntax == 'on' else False
            if type(echo) == str:            
                msg_dict['echo'] = True if echo.lower() == 'true' or echo == 'on' else False
            if type(streaming) == str:            
                msg_dict['streaming'] = True if streaming.lower() == 'true' or streaming == 'on' else False
                
            # Ensure that streamInterval is a float:
            try:
                interval = msg_dict.get('streamInterval', None)
                # If empty string, indicating request for current
                # value, the call to get_or_set_server_parm() will
                # take care of it. But otherwise, floatify:
                if interval != '':
                    msg_dict['streamInterval'] =  float(interval)
            except (ValueError, TypeError):
                self.return_error({}, "Received a non-float for streamInterval from browser: '%s'" % str(interval))
                continue
    
            # Workhorse for servicing the requests:
            response_dict = self.service_browser_request(msg_dict) #@UnusedVariable
            continue
        return
        
    def service_browser_request(self, msg_dict):
        '''
        Given a dict of request key/values from the Web UI,
        set parameters in the test instrument, or return the
        current parameter values, if the values from the UI
        are empty strings. The following example would
        set the topic to which 'one-shot' messages are sent
        to 'myTopic,' and the content of one-shots to a
        string. Given the empty string for strLen, the 
        return dict will contain the current instrument
        length of random strings:
        
        Example:  {'oneShotTopic'   : 'myTopic',
                   'oneShotContent' : 'my oneshot message content',
                   'strLen'         : ''
                   }
        
        :param msg_dict: parameter names and values
        :type msg_dict: {str : str}
        '''
        
        response_dict = {}
        try:
            
            # Are we to fire a one-shot message?
            one_shot_txt = msg_dict.get('oneShot', None)
            if one_shot_txt is not None:
                # If a msg was provided as the value of 
                # the oneShot request, use it; if an empty
                # string was given, use the one-shot msg on
                # record:
                msg_txt = one_shot_txt if len(one_shot_txt) > 0 else self.my_server['oneShotContent']
                
                # Same for destination topic: if supplied, use it, 
                # else use default:
                topic_in_dict = msg_dict.get('oneShotTopic', None)
                msg_topic = topic_in_dict if topic_in_dict is not None else self.my_server['oneShotTopic']
                  
                one_shot_msg = BusMessage(topicName=msg_topic,
                                          content=msg_txt)
                self.my_server.testBus.publish(one_shot_msg)
                response_dict['success'] = 'OK'
                self.write_to_browser(json.dumps(response_dict))
                return
            
            # Go through each server parm in the request dict,
            # and update the server to the respective value;
            # also fill the response_dict with the new value.
            # One UI optimization: do the "length of random string"
            # first, because if subsequent one-shot content or 
            # message-stream content fields are to be changed,
            # They will be the correct length. Without this 
            # order switch, the user needs to hit submit twice:
            
            if msg_dict.get('strLen', None) is not None:
                response_dict = self.get_or_set_server_parm('strLen', msg_dict['strLen'], response_dict)
                del msg_dict['strLen']
                
            for (parm_key, parm_value) in msg_dict.items():
                response_dict = self.get_or_set_server_parm(parm_key, parm_value, response_dict)
            
            # Send a dict with keys being the HTML parameter
            # names, and values being the current server
            # parameter values:
            self.write_to_browser(json.dumps(response_dict))
            
        except ValueError:
            # Was handled in one of the functions called above:
            #******
            raise
            #******
            return response_dict 
        except Exception as e:
            print('Exception in GET: %s' % `e`)
            self.return_error(response_dict, `e`);
            #*********
            raise
            #*********
            return response_dict
        
    def on_bus_message(self, msg=None):
        '''
        Called when a message to which we are subscribed
        comes in. These are msgs on topics explicitly subscribed
        to via the Web UI. If msg is None, check self.bus_msg_queue
        for newly arrived msgs from the bus:
        
        :param msg: message to write to browser.
        :type msgs: {string | None}
        '''
        
        if msg is None:
            # If message/stats queues have not been initialized
            # yet, just return:
            if self.bus_msg_queue is None:
                return
            # We check the msg queue for messages:
            while not self.bus_msg_queue.empty():
                msg = self.bus_msg_queue.get_nowait()
                try:
                    self.write_to_browser({"inmsg" : msg + '\r\n'})
                except Exception as e:
                    self.logErr("Error during read of msg or stats queue from OnDemandServer: '%s'" % `e`)
                    return
        else:
            try:
                self.write_to_browser({"inmsg" : msg + '\r\n'})
            except Exception as e:
                self.logErr("Error during read of msg or stats queue from OnDemandServer: '%s'" % `e`)
                return
            
        # Check for new bus statistics to forward to browser:        
        self.on_bus_stats()

    def on_bus_stats(self, msg=None):
        '''
        Called when stats about messages to which we are subscribed
        come in. These are msgs on topics explicitly subscribed
        to via the Web UI. If msg is None, check self.bus_stats_queue
        for newly arrived msgs from the bus:
        
        :param msg: stats message to write to browser.
        :type msgs: {string | None}
        '''
        
        if msg is None:
            # We check the stats queue for messages:
            while not self.bus_stats_queue.empty():
                msg = self.bus_stats_queue.get_nowait()
                try:
                    self.write_to_browser({"instat" : msg + '\r\n'})
                except Exception as e:
                    self.logErr("Error during read of msg or stats queue from OnDemandServer: '%s'" % `e`)              
                    return
        else:
            try:
                self.write_to_browser({"instat" : msg + '\r\n'})
            except Exception as e:
                self.logErr("Error during read of msg or stats queue from OnDemandServer: '%s'" % `e`)              
                return
            
    def return_error(self, response_dict, error_str):
        '''
        Add entry {'error' : <error_str>} to the 
        passed-in response_dict, and cause
        that dict to be  to be written to the browser via the standing
        websocket.
        
        :param response_dict: dict with parameter name/values
            that worked before the error occurred.
        :type response_dict: {str : str}
        :param error_str: error message
        :type error_str: str
        '''
        response_dict['error'] = error_str
        self.write_to_browser(json.dumps(response_dict))

    def get_or_set_server_parm(self, parm_name, parm_val, response_dict):
        '''
        Given one parm/value pair, get or set the 
        corresponding parameter in the test instrument.
        
        :param parm_name: name of instrument parameter
        :type parm_name: str
        :param parm_val: new value or empty string, if current value is requested
        :type parm_val: str
        :param response_dict: current response dict. This method will add
            to the passed-in dict, and return the augmented dict.
        :type response_dict: {str : str}
        :return: augmented result dict destined for the browser once all
            requests have been processed.
        :rtype: {str : str}
        '''
        
        # Most parameters are singleton arrays.
        # Make those into non-arrays:
        if type(parm_val) == list and len(parm_val) == 1:
            parm_val = parm_val[0]
            
        try:
            # Is this a request for the current value of the server setting?
            # Special cases: oneShotContent and echoContent: setting
            # values of length 0 means: set to default strings of standard
            # length, so the next branch of this conditional is the
            # one to take for these cases; other zero-length values
            # indicate request for current value:
            if len(str(parm_val)) == 0 and \
                   self.my_server is not None and \
                   parm_name != 'oneShotContent' and \
                   parm_name != 'echoContent' and \
                   parm_name != 'streamContent' and \
                   parm_name != 'topicsToRx':
                # Return current value:
                response_dict[parm_name] =  self.my_server[parm_name]
                return response_dict
            
            # We are to set the server parm:
            if self.my_server is not None:
                # Only change if different:
                if self.my_server[parm_name] != parm_val:
                    
                    #********
                    # Because of the instability of subscribe/unsubscribe
                    # during stream publishing: pause streaming if it's
                    # on:
                    was_streaming = False
                    if parm_name != 'streaming' and self.my_server.streaming:
                        was_streaming = True
                        self.my_server.streaming = False
                    #********
                    
                    self.my_server[parm_name] = parm_val
                    
                    #************
                    # Turn streaming back on if it was happening:
                    if was_streaming:
                        self.my_server.streaming = True
                    #************
                    
                    # Read value back out from the server,
                    # b/c the setters may modify values (e.g.
                    # entering defaults when strings are empty):
                    response_dict[parm_name] =  self.my_server[parm_name]
                else:
                    response_dict[parm_name] =  parm_val
            else:
                # It's a request for current value, but no server is running;
                # report back to the browser, and close the connection:
                self.return_error('SchoolBus server is not running.')
                # The following will be ignored in the POST method:
                raise ValueError('SchoolBus server is not running.')
        except KeyError:
            self.return_error(response_dict, "Server parameter '%s' does not exist" % parm_name)
            raise ValueError("Server parameter '%s' does not exist" % parm_name)
        except ValueError as e:
            self.return_error(response_dict, `e`)
            # Re-raising ValueError ensures that caller
            # knows we already returned an error to the
            # browser
            raise
            
        return response_dict

    @property
    def my_server(self):
        '''
        Return an OnDemandServer bus server singleton.
        '''
        
        if self.bus_service is not None:
            return self.bus_service
        
        # Create an OnDemandPubliser that will stream on
        # the standard topic/content:
         
        self.bus_service = OnDemandPublisher(streamMsgs=(None,None))
        
        # Queues into which the server will place msgs that
        # arrive on topics subscribed to through the Web UI:
        self.bus_msg_queue = self.bus_service.bus_msg_queue
        self.bus_stats_queue = self.bus_service.bus_stats_queue
        
        # Before starting the bus server, pause its message streaming server:
        self.bus_service.streaming = False
        self.bus_service.start()
        self.bus_service.serve_echo = True
        self.bus_service.check_syntax = True
        return self.bus_service
        
    def server_running(self):
        '''
        Return True/False for whether the underlying 
        OnDemandPublisher instance, i.e. the test instrument
        is running.
        :return: test instrument running status
        :rtype: bool
        '''
        try:
            return self.bus_service is not None and self.bus_service.running
        except Exception:
            return False 

    def ensure_lower_non_array(self, val):
        '''
        Given a string, or array of strings, return
        an array (possibly containing just the passed-in
        string), in which all strings are lower case:
        
        :param val: input value
        :type val: {str | [str]}
        '''
        if type(val) == list:
            new_val = val[0].lower()
        else:
            new_val = val.lower()
        return new_val
        
    def logInfo(self, msg):
        self.websocket_comm_obj.logInfo(msg)

    def logErr(self, msg):
        self.websocket_comm_obj.logErr(msg)

    def logDebug(self, msg):
        self.websocket_comm_obj.logDebug(msg)