Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restructured roswww webserver #18

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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}
)
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
roswww
======

roswww and roswww_pack. Convenient tool to develop the web apps under ROS infrastructure
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
1 change: 1 addition & 0 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<buildtool_depend>catkin</buildtool_depend>
<run_depend>rosbridge_server</run_depend>
<run_depend>rospack</run_depend>
<run_depend>rospy</run_depend>

<export/>
Expand Down
152 changes: 21 additions & 131 deletions script/webserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
37 changes: 37 additions & 0 deletions src/roswww/__init__.py
Original file line number Diff line number Diff line change
@@ -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
166 changes: 166 additions & 0 deletions src/roswww/roswww_server.py
Original file line number Diff line number Diff line change
@@ -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))
Loading