You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
scriptform/src/scriptform.py

211 lines
7.8 KiB

10 years ago
#!/usr/bin/env python
10 years ago
# -*- coding: utf-8 -*-
10 years ago
# Scriptform roughly works like this:
#
# 1. Instantiate a ScriptForm class. This takes care of loading the form config
# (json) file and provides methods to run the server.
# 2. If running as a daemon:
# a) Instantiate the Daemon class
# b) Hook up a callback to shutdown the ScriptForm server
# c) Start the daemon. This detaches from the console.
# 3. Start the ScriptForm server. This listens on a port for incoming HTTP
# connections.
# 4. If a request comes in, it is dispatched to the ScriptFormWebApp request
# handler.ScriptFormWebApp inherits from the WebAppHandler class. The
# WebAppHandler determines which method of ScriptFormWebApp the request
# should be dispatched to.
# 5. Depending on the request, a method is called on ScriptFormWebApp. These
# methods render HTML to as a response.
# 6. If a form is submitted, its fields are validated and the script callback
# is called. Depending on the output type, the output of the script is either
# captured and displayed as HTML to the user or directly streamed to the
# browser.
# 7. GOTO 4.
# 8. Upon receiving an OS signal (kill, etc) the daemon calls the shutdown
# callback.
# 9. The shutdown callback starts a new thread (otherwise the webserver blocks
# until the next request) to stop the server.
# 10. The program exits.
10 years ago
import sys
import optparse
import os
import json
import logging
import thread
import hashlib
10 years ago
from daemon import Daemon
from formdefinition import FormDefinition
from formconfig import FormConfig
from webapp import ThreadedHTTPServer, ScriptFormWebApp
class ScriptFormError(Exception):
pass
class ScriptForm:
"""
'Main' class that orchestrates parsing the Form configurations and running
the webserver.
"""
def __init__(self, config_file, cache=True):
self.config_file = config_file
self.cache = cache
self.log = logging.getLogger('SCRIPTFORM')
10 years ago
self.form_config_singleton = None
self.websrv = None
self.running = False
10 years ago
self.httpd = None
self.get_form_config() # Init form config so it can raise errors about problems.
def get_form_config(self):
"""
Read and return the form configuration in the form of a FormConfig
instance. If it has already been read, a cached version is returned.
"""
# Cache
10 years ago
if self.cache and self.form_config_singleton is not None:
return self.form_config_singleton
config = json.load(file(self.config_file, 'r'))
static_dir = None
custom_css = None
users = None
forms = []
if 'static_dir' in config:
static_dir = config['static_dir']
if 'custom_css' in config:
custom_css = file(config['custom_css'], 'r').read()
if 'users' in config:
users = config['users']
for form in config['forms']:
form_name = form['name']
script = form['script']
forms.append(
FormDefinition(form_name,
form['title'],
form['description'],
form['fields'],
script,
output=form.get('output', 'escaped'),
hidden=form.get('hidden', False),
submit_title=form.get('submit_title', 'Submit'),
allowed_users=form.get('allowed_users', None))
)
form_config = FormConfig(
config['title'],
forms,
users,
static_dir,
custom_css
)
self.form_config_singleton = form_config
return form_config
def run(self, listen_addr='0.0.0.0', listen_port=80):
"""
Start the webserver on address `listen_addr` and port `listen_port`.
This call is blocking until the user hits Ctrl-c, the shutdown() method
is called or something like SystemExit is raised in a handler.
"""
ScriptFormWebApp.scriptform = self
self.httpd = ThreadedHTTPServer((listen_addr, listen_port), ScriptFormWebApp)
self.httpd.daemon_threads = True
self.log.info("Listening on {0}:{1}".format(listen_addr, listen_port))
self.running = True
self.httpd.serve_forever()
self.running = False
def shutdown(self):
self.log.info("Attempting server shutdown")
def t_shutdown(sf):
sf.log.info(self.websrv)
sf.httpd.socket.close() # Undocumented requirement to shut the server
sf.httpd.shutdown()
# We need to spawn a new thread in which the server is shut down,
# because doing it from the main thread blocks, since the server is
# wainting for connections..
thread.start_new_thread(t_shutdown, (self, ))
10 years ago
def main(): # pragma: no cover
usage = [
sys.argv[0] + " [option] (--start|--stop) <form_definition.json>",
" " + sys.argv[0] + " --generate-pw",
]
parser = optparse.OptionParser(version="%%VERSION%%")
parser.set_usage('\n'.join(usage))
10 years ago
parser.add_option("-g", "--generate-pw", dest="generate_pw",
action="store_true", default=False,
help="Generate password")
parser.add_option("-p", "--port", dest="port", action="store", type="int",
default=80, help="Port to listen on")
parser.add_option("-f", "--foreground", dest="foreground",
action="store_true", default=False,
help="Run in foreground (debugging)")
parser.add_option("-r", "--reload", dest="reload", action="store_true",
default=False,
help="Reload form config on every request (DEV)")
parser.add_option("--pid-file", dest="pid_file", action="store",
default=None, help="Pid file")
parser.add_option("--log-file", dest="log_file", action="store",
default=None, help="Log file")
parser.add_option("--start", dest="action_start", action="store_true",
default=None, help="Start daemon")
parser.add_option("--stop", dest="action_stop", action="store_true",
default=None, help="Stop daemon")
(options, args) = parser.parse_args()
if options.generate_pw:
# Generate a password for use in the `users` section
import getpass
plain_pw = getpass.getpass()
if not plain_pw == getpass.getpass('Repeat password: '):
sys.stderr.write("Passwords do not match.\n")
sys.exit(1)
sys.stdout.write(hashlib.sha256(plain_pw).hexdigest() + '\n')
sys.exit(0)
else:
if not options.action_stop and len(args) < 1:
parser.error("Insufficient number of arguments")
if not options.action_stop and not options.action_start:
options.action_start = True
# If a form configuration was specified, change to that dir so we can
# find the job scripts and such.
if len(args) > 0:
path = os.path.dirname(args[0])
if path:
os.chdir(path)
args[0] = os.path.basename(args[0])
daemon = Daemon(options.pid_file, options.log_file,
foreground=options.foreground)
log = logging.getLogger('MAIN')
try:
if options.action_start:
sf = ScriptForm(args[0], cache=not options.reload)
daemon.register_shutdown_cb(sf.shutdown)
daemon.start()
sf.run(listen_port=options.port)
elif options.action_stop:
daemon.stop()
sys.exit(0)
except Exception, e:
log.exception(e)
raise
10 years ago
if __name__ == "__main__": # pragma: no cover
10 years ago
main()