diff --git a/CMakeLists.txt b/CMakeLists.txt index 92371a5..478677c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,4 +4,8 @@ find_package(catkin REQUIRED) catkin_package() catkin_python_setup() -install(DIRECTORY script DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} USE_SOURCE_PERMISSIONS) +install( + PROGRAMS + script/webserver.py + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) diff --git a/README.md b/README.md index 00fce70..edae15c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ roswww ====== -roswww and roswww_pack. Convenient tool to develop the web apps under ROS infrastructure \ No newline at end of file +roswww Convenient tool to develop the web apps under ROS infrastructure + +## Parameters ## + +* `--port` : Web server port +* `--name` : Web server name +* `--webpath` : relative path to web page diff --git a/package.xml b/package.xml index f586a08..8840ee5 100644 --- a/package.xml +++ b/package.xml @@ -14,6 +14,7 @@ catkin rosbridge_server + rospack rospy diff --git a/script/webserver.py b/script/webserver.py index a8339d7..b76abb4 100755 --- a/script/webserver.py +++ b/script/webserver.py @@ -34,139 +34,29 @@ # # Author: Jonathan Mace, Jihoon Lee, Isaac Isao Saito -import socket -import subprocess -import tornado.ioloop # rosbridge installs tornado -import tornado.web +import sys +import argparse +import roswww -import rospy +def parse_argument(argv): + """ + argument parser for roswww server configuration + """ + parser = argparse.ArgumentParser(description="ROSWWW Server") + parser.add_argument('-n', '--name', default='80', help='Webserver name') + parser.add_argument('-p', '--port', default='80', help='Webserver Port number') + parser.add_argument('-w', '--webpath', default='www', help='package relative path to web pages') + parser.add_argument('--start_port', default='8000', help='setting up port scan range') + parser.add_argument('--end_port', default='9000', help='setting up port scan range') + parsed_args = parser.parse_args(argv) -from roswww.webrequest_handler import WebRequestHandler - - -def run_shellcommand(*args): - '''run the provided command and return its stdout''' - args = sum([(arg if type(arg) == list else [arg]) for arg in args], []) - return subprocess.Popen(args, - stdout=subprocess.PIPE).communicate()[0].strip() - -def split_words(text): - '''return a list of lines where each line is a list of words''' - return [line.strip().split() for line in text.split('\n')] - -def get_packages(): - ''' - Find the names and locations of all ROS packages - - @rtype: {str, str} - @return: name and path of ROS packages - ''' - lines = split_words(run_shellcommand('rospack', 'list')) - packages = [{'name': name, 'path': path} for name, path in lines] - return packages - -def create_webserver(packages): - ''' - @type packages: {str, str} - @param packages: name and path of ROS packages. - ''' - handlers = [(r"/", WebRequestHandler, {"packages": packages})] - - for package in packages: - handler_root = ("/" + package['name'] + "/?()", - tornado.web.StaticFileHandler, - {"path": package['path'] + "/www/index.html"}) - handlers.append(handler_root) - handler = ("/" + package['name'] + "/(.*)", - tornado.web.StaticFileHandler, - {"path": package['path'] + "/www", - "default_filename": "index.html"}) - handlers.append(handler) - - rospy.loginfo("Webserver initialized for %d packages", len(packages)) - application = tornado.web.Application(handlers) - - return application - -def bind_webserver(application): - """ See if there's a default port, use 80 if not """ - default_port, start_port, end_port = get_webserver_params() - - """ First, we try the default http port 80 """ - bound = bind_to_port(application, default_port) - - if not bound: - """ Otherwise bind any available port within the specified range """ - bound = bind_in_range(application, start_port, end_port) - - return bound - -def get_webserver_params(): - try: - default_port = rospy.get_param("http/default", 80) - start_port = rospy.get_param("http/range_start", 8000) - end_port = rospy.get_param("http/range_end", 9000) - return (default_port, start_port, end_port) - except socket.error as err: - if err.errno == 111: - # Roscore isn't started or cannot be contacted - rospy.logwarn("Could not contact ROS master." + \ - " Is a roscore running? Error: %s", err.strerror) - return 80, 8000, 9000 - else: - raise - -def start_webserver(application): - try: - tornado.ioloop.IOLoop.instance().start() - except KeyboardInterrupt: - rospy.loginfo("Webserver shutting down") - - -def bind_to_port(application, portno): - rospy.loginfo("Attempting to start webserver on port %d", portno) - try: - application.listen(portno) - rospy.loginfo("Webserver successfully started on port %d", portno) - except socket.error as err: - # Socket exceptions get handled, all other exceptions propagated - if err.errno == 13: - rospy.logwarn("Insufficient priveliges to run webserver " + - "on port %d. Error: %s", portno, err.strerror) - rospy.loginfo("-- Try re-running as super-user: sudo su; " + - "source ~/.bashrc)") - elif err.errno == 98: - rospy.logwarn("There is already a webserver running on port %d. " + - "Error: %s", portno, err.strerror) - rospy.loginfo("-- Try stopping your web server. For example, " + - "to stop apache: sudo /etc/init.d/apache2 stop") - else: - rospy.logerr("An error occurred attempting to listen on " + - "port %d: %s", portno, err.strerror) - return False - return True - - -def bind_in_range(application, start_port, end_port): - if (end_port > start_port): - for i in range(start_port, end_port): - if bind_to_port(application, i): - return True - return False - - -def run_webserver(): - try: - packages = get_packages() - server = create_webserver(packages) - bound = bind_webserver(server) - if (bound): - start_webserver(server) - else: - raise Exception() - except Exception as exc: - rospy.logerr("Unable to bind webserver. Exiting. %s" % exc) + return parsed_args.name, parsed_args.webpath, (parsed_args.port, parsed_args.start_port, parsed_args.end_port) if __name__ == '__main__': - run_webserver() + argv = sys.argv + name, webpath, port = parse_argument(argv[1:]) + + webserver = roswww.ROSWWWServer(name, webpath, port) + webserver.loginfo("Initialised") + webserver.spin() diff --git a/src/roswww/__init__.py b/src/roswww/__init__.py index e69de29..0b994b4 100644 --- a/src/roswww/__init__.py +++ b/src/roswww/__init__.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +# Software License Agreement (BSD License) +# +# Copyright (c) 2013, Tokyo Opensource Robotics Kyokai Association +# 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 Tokyo Opensource Robotics Kyokai Association. 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 OWNER 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. +# +# Author: Jonathan Mace, Jihoon Lee, Isaac Isao Saito + +from .roswww_server import ROSWWWServer diff --git a/src/roswww/roswww_server.py b/src/roswww/roswww_server.py new file mode 100644 index 0000000..6d3c425 --- /dev/null +++ b/src/roswww/roswww_server.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python + +# Software License Agreement (BSD License) +# +# Copyright (c) 2013, Tokyo Opensource Robotics Kyokai Association +# 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 Tokyo Opensource Robotics Kyokai Association. 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 OWNER 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. +# +# Author: Jonathan Mace, Jihoon Lee, Isaac Isao Saito + +import logging + +import socket +import tornado.ioloop # rosbridge installs tornado +import tornado.web +from .webrequest_handler import WebRequestHandler +from .utils import run_shellcommand, split_words, get_packages + +class ROSWWWServer(): + + def __init__(self, name, webpath, ports): + ''' + :param str name: webserver name + :param str webpath: package relative path to web page source. + :param tuple ports: ports to use in webserver. Provides default and scan range (default, start, end) + ''' + self._name = name + self._webpath = webpath + self._ports = ports + self._logger = self._set_logger() + self._packages = get_packages() + self._application = self._create_webserver(self._packages) + + def _create_webserver(self, packages): + ''' + @type packages: {str, str} + @param packages: name and path of ROS packages. + ''' + handlers = [(r"/", WebRequestHandler, {"packages": packages})] + + for package in packages: + handler_root = ("/" + package['name'] + "/?()", + tornado.web.StaticFileHandler, + {"path": package['path'] + "/" + self._webpath + "/index.html"}) + handlers.append(handler_root) + handler = ("/" + package['name'] + "/(.*)", + tornado.web.StaticFileHandler, + {"path": package['path'] + "/" + self._webpath, + "default_filename": "index.html"}) + handlers.append(handler) + + self.loginfo("# of packages : %s"%(len(packages))) + self.loginfo("Weg Page root : %s"%(self._webpath)) + application = tornado.web.Application(handlers) + return application + + def _bind_webserver(self): + default, start, end = self._ports + + """ First, we try the default http port """ + bound = self._bind_to_port(self._application, default) + if not bound: + """ Otherwise bind any available port within the specified range """ + bound = self._bind_in_range(self._application, start, end) + return True + + def _bind_in_range(self, application, start_port, end_port): + if (end_port > start_port): + for i in range(start_port, end_port): + if self._bind_to_port(application, i): + return True + return False + + def _bind_to_port(self, application, portno): + self.loginfo("Attempting to start webserver on port %s"%portno) + try: + application.listen(portno) + self.loginfo("Webserver successfully started on port %s"%portno) + except socket.error as err: + # Socket exceptions get handled, all other exceptions propagated + if err.errno == 13: + self.logwarn("Insufficient priveliges to run webserver " + + "on port %s. Error: %s"%(portno, err.strerror)) + self.loginfo("-- Try re-running as super-user: sudo su; " + + "source ~/.bashrc)") + elif err.errno == 98: + self.logwarn("There is already a webserver running on port %s. " + + "Error: %s"%(portno, err.strerror)) + self.loginfo("-- Try stopping your web server. For example, " + + "to stop apache: sudo /etc/init.d/apache2 stop") + else: + self.logerr("An error occurred attempting to listen on " + + "port %s: %s"%(portno, err.strerror)) + return False + return True + + def _start_webserver(self): + try: + tornado.ioloop.IOLoop.instance().start() + except KeyboardInterrupt: + self.loginfo("Webserver shutting down") + + def spin(self): + try: + bound = self._bind_webserver() + if bound: + self._start_webserver() + else: + raise Exception() + except Exception as exc: + self.logerr("Unable to bind webserver. Exiting. %s" % exc) + + def _set_logger(self): + logger = logging.getLogger('roswww') + logger.setLevel(logging.DEBUG) + + # create console handler and set level to debug + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG) + + # create formatter + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + + # add formatter to ch + ch.setFormatter(formatter) + + # add ch to logger + logger.addHandler(ch) + + return logger + + + def loginfo(self, msg): + self._logger.info('%s : %s'%(self._name, msg)) + + def logwarn(self, msg): + self._logger.warning('%s : %s'%(self._name, msg)) + + def logerr(self, msg): + self._logger.error('%s : %s'%(self._name, msg)) diff --git a/src/roswww/utils.py b/src/roswww/utils.py new file mode 100644 index 0000000..c540ef0 --- /dev/null +++ b/src/roswww/utils.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +# Software License Agreement (BSD License) +# +# Copyright (c) 2013, Tokyo Opensource Robotics Kyokai Association +# 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 Tokyo Opensource Robotics Kyokai Association. 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 OWNER 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. +# +# Author: Jonathan Mace, Jihoon Lee, Isaac Isao Saito + +############################################# +# Methods +############################################# +import subprocess + +def get_packages(): + ''' + Find the names and locations of all ROS packages + + @rtype: {str, str} + @return: name and path of ROS packages + ''' + lines = split_words(run_shellcommand('rospack', 'list')) + packages = [{'name': name, 'path': path} for name, path in lines] + return packages + +def run_shellcommand(*args): + '''run the provided command and return its stdout''' + args = sum([(arg if type(arg) == list else [arg]) for arg in args], []) + return subprocess.Popen(args, + stdout=subprocess.PIPE).communicate()[0].strip() + +def split_words(text): + '''return a list of lines where each line is a list of words''' + return [line.strip().split() for line in text.split('\n')] + diff --git a/src/roswww/webrequest_handler.py b/src/roswww/webrequest_handler.py index 6e25263..ab83228 100755 --- a/src/roswww/webrequest_handler.py +++ b/src/roswww/webrequest_handler.py @@ -36,7 +36,6 @@ import tornado.web - class WebRequestHandler(tornado.web.RequestHandler): def initialize(self, packages):