From 1c08c07a0f255e80d11c7eb6ec8ab47799f7531a Mon Sep 17 00:00:00 2001 From: mdipierro Date: Wed, 1 May 2019 21:07:52 -0700 Subject: [PATCH] new command line options --- CHANGELOG | 21 + docker/alpine/web2py-rocket-ssl/Dockerfile | 2 +- docker/alpine/web2py-rocket/Dockerfile | 2 +- docker/centos/web2py-rocket/Dockerfile | 2 +- docker/debian/web2py-rocket/Dockerfile | 2 +- docker/fedora/web2py-rocket/Dockerfile | 2 +- docker/opensuse/web2py-rocket/Dockerfile | 2 +- docker/python/web2py-rocket-ssl/Dockerfile | 2 +- docker/python/web2py-rocket/Dockerfile | 2 +- .../stack/web2py-rocket-nginx/web2py-rocket | 2 +- .../web2py-rocket-ssl | 2 +- .../web2py-rocket-ssl | 2 +- .../web2py-rocket-ssl | 2 +- .../web2py-rocket-ssl-nginx/web2py-rocket-ssl | 2 +- docker/ubuntu/web2py-rocket/Dockerfile | 2 +- gluon/console.py | 712 ++++++++++++++++++ gluon/fileutils.py | 3 +- gluon/globals.py | 9 +- gluon/main.py | 4 +- gluon/newcron.py | 7 +- gluon/packages/yatl | 2 +- gluon/shell.py | 10 +- gluon/tests/test_scheduler.py | 3 +- gluon/widget.py | 417 ++-------- scripts/web2py.fedora.sh | 2 +- 25 files changed, 809 insertions(+), 409 deletions(-) create mode 100644 gluon/console.py diff --git a/CHANGELOG b/CHANGELOG index b5909b91..47b185a4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,24 @@ +## 2.19.0 +- new command line options (Thanks Paolo Pastori) + +OLD NAME NEW NAME +================== ================== +--debug --log_level +--nogui --no_gui +--ssl_private_key --server_key +--ssl_certificate --server_cert +--minthreads --min_threads +--maxthreads --max_threads +--profiler --profiler_dir +--run-cron --with_cron +--softcron --soft_cron +--cron --cron_run +--cronjob * --cron_job * +--test --run_doctests + --add_options + --interface + --crontab + ## 2.18.1-2.18.5 - pydal 19.04 - made template its own module (Yet Another Template Language) diff --git a/docker/alpine/web2py-rocket-ssl/Dockerfile b/docker/alpine/web2py-rocket-ssl/Dockerfile index 05a8c47d..f0d34fd1 100755 --- a/docker/alpine/web2py-rocket-ssl/Dockerfile +++ b/docker/alpine/web2py-rocket-ssl/Dockerfile @@ -19,4 +19,4 @@ WORKDIR /web2py EXPOSE 443 -CMD python /web2py/web2py.py --nogui --no-banner -a 'a' -c web2py.crt -k web2py.key -i 0.0.0.0 -p 443 +CMD python /web2py/web2py.py --no_gui --no_banner -a 'a' -k web2py.key -c web2py.crt -i 0.0.0.0 -p 443 diff --git a/docker/alpine/web2py-rocket/Dockerfile b/docker/alpine/web2py-rocket/Dockerfile index 404e0f45..dd08901a 100755 --- a/docker/alpine/web2py-rocket/Dockerfile +++ b/docker/alpine/web2py-rocket/Dockerfile @@ -24,4 +24,4 @@ WORKDIR /home/web2py/web2py EXPOSE 8000 -CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --nogui --no-banner -a 'a' -i 0.0.0.0 -p 8000 +CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --no_gui --no_banner -a 'a' -i 0.0.0.0 -p 8000 diff --git a/docker/centos/web2py-rocket/Dockerfile b/docker/centos/web2py-rocket/Dockerfile index 212f84bb..bd86aad4 100755 --- a/docker/centos/web2py-rocket/Dockerfile +++ b/docker/centos/web2py-rocket/Dockerfile @@ -25,4 +25,4 @@ WORKDIR /home/web2py/web2py EXPOSE 8000 -CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --nogui --no-banner -a 'a' -i 0.0.0.0 -p 8000 +CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --no_gui --no_banner -a 'a' -i 0.0.0.0 -p 8000 diff --git a/docker/debian/web2py-rocket/Dockerfile b/docker/debian/web2py-rocket/Dockerfile index 802491cc..43c3164d 100755 --- a/docker/debian/web2py-rocket/Dockerfile +++ b/docker/debian/web2py-rocket/Dockerfile @@ -25,4 +25,4 @@ WORKDIR /home/web2py/web2py EXPOSE 8000 -CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --nogui --no-banner -a 'a' -i 0.0.0.0 -p 8000 +CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --no_gui --no_banner -a 'a' -i 0.0.0.0 -p 8000 diff --git a/docker/fedora/web2py-rocket/Dockerfile b/docker/fedora/web2py-rocket/Dockerfile index 3fa20078..bdd1db99 100755 --- a/docker/fedora/web2py-rocket/Dockerfile +++ b/docker/fedora/web2py-rocket/Dockerfile @@ -24,4 +24,4 @@ WORKDIR /home/web2py/web2py EXPOSE 8000 -CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --nogui --no-banner -a 'a' -i 0.0.0.0 -p 8000 +CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --no_gui --no_banner -a 'a' -i 0.0.0.0 -p 8000 diff --git a/docker/opensuse/web2py-rocket/Dockerfile b/docker/opensuse/web2py-rocket/Dockerfile index a8fca1e2..24797491 100755 --- a/docker/opensuse/web2py-rocket/Dockerfile +++ b/docker/opensuse/web2py-rocket/Dockerfile @@ -24,4 +24,4 @@ WORKDIR /home/web2py/web2py EXPOSE 8000 -CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --nogui --no-banner -a 'a' -i 0.0.0.0 -p 8000 +CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --no_gui --no_banner -a 'a' -i 0.0.0.0 -p 8000 diff --git a/docker/python/web2py-rocket-ssl/Dockerfile b/docker/python/web2py-rocket-ssl/Dockerfile index 9308ca4b..8f238582 100755 --- a/docker/python/web2py-rocket-ssl/Dockerfile +++ b/docker/python/web2py-rocket-ssl/Dockerfile @@ -18,4 +18,4 @@ WORKDIR /web2py EXPOSE 443 -CMD python /web2py/web2py.py --nogui --no-banner -a 'a' -c web2py.crt -k web2py.key -i 0.0.0.0 -p 443 +CMD python /web2py/web2py.py --no_gui --no_banner -a 'a' -k web2py.key -c web2py.crt -i 0.0.0.0 -p 443 diff --git a/docker/python/web2py-rocket/Dockerfile b/docker/python/web2py-rocket/Dockerfile index 28058453..94fdf95e 100755 --- a/docker/python/web2py-rocket/Dockerfile +++ b/docker/python/web2py-rocket/Dockerfile @@ -20,4 +20,4 @@ WORKDIR /home/web2py/web2py EXPOSE 8000 -CMD python /home/web2py/web2py/web2py.py --nogui --no-banner -a 'a' -i 0.0.0.0 -p 8000 +CMD python /home/web2py/web2py/web2py.py --no_gui --no_banner -a 'a' -i 0.0.0.0 -p 8000 diff --git a/docker/stack/web2py-rocket-nginx/web2py-rocket b/docker/stack/web2py-rocket-nginx/web2py-rocket index ed228fcf..5b64b076 100644 --- a/docker/stack/web2py-rocket-nginx/web2py-rocket +++ b/docker/stack/web2py-rocket-nginx/web2py-rocket @@ -22,4 +22,4 @@ WORKDIR /home/web2py/web2py EXPOSE 8000 -CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --nogui --no-banner -a 'a' -i 0.0.0.0 -p 8000 +CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --no_gui --no_banner -a 'a' -i 0.0.0.0 -p 8000 diff --git a/docker/stack/web2py-rocket-ssl-nginx-db-adminer/web2py-rocket-ssl b/docker/stack/web2py-rocket-ssl-nginx-db-adminer/web2py-rocket-ssl index 5a7ffe54..1d6629b7 100644 --- a/docker/stack/web2py-rocket-ssl-nginx-db-adminer/web2py-rocket-ssl +++ b/docker/stack/web2py-rocket-ssl-nginx-db-adminer/web2py-rocket-ssl @@ -17,4 +17,4 @@ WORKDIR /web2py EXPOSE 443 -CMD python /web2py/web2py.py --nogui --no-banner -a 'a' -c web2py.crt -k web2py.key -i 0.0.0.0 -p 443 +CMD python /web2py/web2py.py --no_gui --no_banner -a 'a' -k web2py.key -c web2py.crt -i 0.0.0.0 -p 443 diff --git a/docker/stack/web2py-rocket-ssl-nginx-memcached/web2py-rocket-ssl b/docker/stack/web2py-rocket-ssl-nginx-memcached/web2py-rocket-ssl index 6720178a..6f271006 100644 --- a/docker/stack/web2py-rocket-ssl-nginx-memcached/web2py-rocket-ssl +++ b/docker/stack/web2py-rocket-ssl-nginx-memcached/web2py-rocket-ssl @@ -17,4 +17,4 @@ WORKDIR /web2py EXPOSE 443 -CMD python /web2py/web2py.py --nogui --no-banner -a 'a' -c web2py.crt -k web2py.key -i 0.0.0.0 -p 443 +CMD python /web2py/web2py.py --no_gui --no_banner -a 'a' -k web2py.key -c web2py.crt -i 0.0.0.0 -p 443 diff --git a/docker/stack/web2py-rocket-ssl-nginx-redis/web2py-rocket-ssl b/docker/stack/web2py-rocket-ssl-nginx-redis/web2py-rocket-ssl index 0b8c82f9..c6bb03fc 100644 --- a/docker/stack/web2py-rocket-ssl-nginx-redis/web2py-rocket-ssl +++ b/docker/stack/web2py-rocket-ssl-nginx-redis/web2py-rocket-ssl @@ -17,4 +17,4 @@ WORKDIR /web2py EXPOSE 443 -CMD python /web2py/web2py.py --nogui --no-banner -a 'a' -c web2py.crt -k web2py.key -i 0.0.0.0 -p 443 +CMD python /web2py/web2py.py --no_gui --no_banner -a 'a' -k web2py.key -c web2py.crt -i 0.0.0.0 -p 443 diff --git a/docker/stack/web2py-rocket-ssl-nginx/web2py-rocket-ssl b/docker/stack/web2py-rocket-ssl-nginx/web2py-rocket-ssl index 6720178a..6f271006 100644 --- a/docker/stack/web2py-rocket-ssl-nginx/web2py-rocket-ssl +++ b/docker/stack/web2py-rocket-ssl-nginx/web2py-rocket-ssl @@ -17,4 +17,4 @@ WORKDIR /web2py EXPOSE 443 -CMD python /web2py/web2py.py --nogui --no-banner -a 'a' -c web2py.crt -k web2py.key -i 0.0.0.0 -p 443 +CMD python /web2py/web2py.py --no_gui --no_banner -a 'a' -k web2py.key -c web2py.crt -i 0.0.0.0 -p 443 diff --git a/docker/ubuntu/web2py-rocket/Dockerfile b/docker/ubuntu/web2py-rocket/Dockerfile index ea48283d..ab96dca2 100755 --- a/docker/ubuntu/web2py-rocket/Dockerfile +++ b/docker/ubuntu/web2py-rocket/Dockerfile @@ -24,4 +24,4 @@ WORKDIR /home/web2py/web2py EXPOSE 8000 -CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --nogui --no-banner -a 'a' -i 0.0.0.0 -p 8000 +CMD . /home/web2py/bin/activate && python /home/web2py/web2py/web2py.py --no_gui --no_banner -a 'a' -i 0.0.0.0 -p 8000 diff --git a/gluon/console.py b/gluon/console.py new file mode 100644 index 00000000..5a17c52f --- /dev/null +++ b/gluon/console.py @@ -0,0 +1,712 @@ +# -*- coding: utf-8 -*- +# vim: set ts=4 sw=4 et ai: +""" +| This file is part of the web2py Web Framework +| Copyrighted by Massimo Di Pierro +| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) + +Command line interface +---------------------- + +The processing of all command line arguments is done using +the argparse library in the console function. + +The basic principle is to process and check for all options +in a single place, this place is the parse_args function. +Notice that when I say all options I mean really all, +options sourced from a configuration file are included. + +A brief summary of options style follows, +for the benefit of code maintainers/developers: + +- use the underscore to split words in long names (as in + '--run_system_tests') +- remember to allow the '-' too as word separator (e.g. + '--run-system-tests') but do not use this form on help +- prefer short names on help messages, instead use + all options names in warning/error messages (e.g. + '-R/--run requires -S/--shell') +""" + +from __future__ import print_function + +__author__ = 'Paolo Pastori' + +import os.path +import argparse +import logging +import socket +import sys +import re +import ast +from collections import OrderedDict +import copy + +from gluon._compat import PY2 +from gluon.shell import die +from gluon.utils import is_valid_ip_address +from gluon.settings import global_settings + + +def warn(msg): + print("%s: warning: %s" % (sys.argv[0], msg), file=sys.stderr) + +def is_appdir(applications_parent, app): + return os.path.isdir(os.path.join(applications_parent, 'applications', app)) + + +def console(version): + """ + Load command line options. + Trivial -h/--help and --version options are also processed. + + Returns a namespace object (in the sense of argparse) + with all options loaded. + """ + + # replacement hints for deprecated options + deprecated_opts = { + '--debug': '--log_level', + '--nogui': '--no_gui', + '--ssl_private_key': '--server_key', + '--ssl_certificate': '--server_cert', + '--interfaces': None, # dest is 'interfaces', hint is '--interface' + '-n': '--min_threads', '--numthreads': '--min_threads', + '--minthreads': '--min_threads', + '--maxthreads': '--max_threads', + '-z': None, '--shutdown_timeout': None, + '--profiler': '--profiler_dir', + '--run-cron': '--with_cron', + '--softcron': '--soft_cron', + '--cron': '--cron_run', + '--test': '--run_doctests' + } + + class HelpFormatter2(argparse.HelpFormatter): + """Hides the options listed in _hidden_options in usage help.""" + + # NOTE: preferred style for long options name is to use '_' + # between words (as in 'no_gui'), also accept the '-' in + # most of the options but do not show both versions on help + _omitted_opts = ('--add-options', '--errors-to-console', + '--no-banner', '--log-level', '--no-gui', '--import-models', + '--server-name', '--server-key', '--server-cert', '--ca-cert', + '--pid-filename', '--log-filename', '--min-threads', + '--max-threads', '--request-queue-size', '--socket-timeout', + '--profiler-dir', '--with-scheduler', '--with-cron', + '--soft-cron', '--cron-run', + '--run-doctests', '--run-system-tests', '--with-coverage') + + _hidden_options = _omitted_opts + tuple(deprecated_opts.keys()) + + def _format_action_invocation(self, action): + if not action.option_strings: + return super(HelpFormatter2, self)._format_action_invocation(action) + parts = [] + if action.nargs == 0: + parts.extend(filter(lambda o : o not in self._hidden_options, + action.option_strings)) + else: + default = action.dest.upper() + args_string = self._format_args(action, default) + for option_string in action.option_strings: + if option_string in self._hidden_options: + continue + parts.append('%s %s' % (option_string, args_string)) + return ', '.join(parts) + + class ExtendAction(argparse._AppendAction): + """Action to accumulate values in a flat list.""" + + def __call__(self, parser, namespace, values, option_string=None): + if isinstance(values, list): + # must copy to avoid altering the option default value + items = argparse._ensure_value(namespace, self.dest, [])[:] + # for options that allows multiple args (i.e. those declared + # with add_argument(..., nargs='+', ...)) the values are + # always placed into a list + while len(values) == 1 and isinstance(values[0], list): + values = values[0] + items.extend(values) + setattr(namespace, self.dest, items) + else: + super(ExtendAction, self).__call__(parser, namespace, values, option_string) + + parser = argparse.ArgumentParser( + usage='python %(prog)s [options]', + description='web2py Web Framework startup script.', + epilog='''NOTE: unless a password is specified (-a 'passwd') +web2py will attempt to run a GUI to ask for it when starting the web server +(if not disabled with --no_gui).''', + formatter_class=HelpFormatter2, + add_help=False) # do not add -h/--help option + + # global options + g = parser.add_argument_group('global options') + g.add_argument('-h', '--help', action='help', + help='show this help message and exit') + g.add_argument('--version', action='version', + version=version, + help="show program's version and exit") + folder = os.getcwd() + g.add_argument('-f', '--folder', + default=folder, metavar='WEB2PY_DIR', + help='web2py installation directory (%(default)s)') + def existing_file(v): + if not v: + raise argparse.ArgumentTypeError('empty argument') + if not os.path.exists(v): + raise argparse.ArgumentTypeError("file %r not found" % v) + return v + g.add_argument('-L', '--config', + type=existing_file, + metavar='PYTHON_FILE', + help='read all options from PYTHON_FILE') + g.add_argument('--add_options', '--add-options', + default=False, + action='store_true', help= + 'add options to existing ones, useful with -L only') + g.add_argument('-a', '--password', + default='', help= + 'password to be used for administration (use "" ' + 'to reuse the last password), when no password is available ' + 'the administrative web interface will be disabled') + g.add_argument('-e', '--errors_to_console', '--errors-to-console', + default=False, + action='store_true', + help='log application errors to console') + g.add_argument('--no_banner', '--no-banner', + default=False, + action='store_true', + help='do not print header banner') + g.add_argument('-Q', '--quiet', + default=False, + action='store_true', + help='disable all output') + integer_log_level = [] + def log_level(v): + # try to convert a lgging level name to its numeric value, + # could use logging.getLevelName but not with + # 3.4 <= Python < 3.4.2, see + # https://docs.python.org/3/library/logging.html#logging.getLevelName) + try: + name2level = logging._levelNames + except AttributeError: + # logging._levelNames has gone with Python 3.4, see + # https://github.com/python/cpython/commit/3b84eae03ebd8122fdbdced3d85999dd9aedfc7e + name2level = logging._nameToLevel + try: + return name2level[v.upper()] + except KeyError: + pass + try: + ill = int(v) + # value deprecated: integer in range(101) + if 0 <= ill <= 100: + integer_log_level.append(ill) + return ill + except ValueError: + pass + raise argparse.ArgumentTypeError("bad level %r" % v) + g.add_argument('-D', '--log_level', '--log-level', + '--debug', # deprecated + default='WARNING', + type=log_level, + metavar='LOG_LEVEL', help= + 'set log level, allowed values are: NOTSET, DEBUG, INFO, WARN, ' + 'WARNING, ERROR, and CRITICAL, also lowercase (default is ' + '%(default)s)') + + # GUI options + g = parser.add_argument_group('GUI options') + g.add_argument('--no_gui', '--no-gui', + '--nogui', # deprecated + default=False, + action='store_true', + help='do not run GUI') + g.add_argument('-t', '--taskbar', + default=False, + action='store_true', + help='run in taskbar (system tray)') + + # console options + g = parser.add_argument_group('console options') + g.add_argument('-S', '--shell', + metavar='APP_ENV', help= + 'run web2py in Python interactive shell or IPython (if installed) ' + 'with specified application environment (if application does not ' + 'exist it will be created). APP_ENV like a/c/f?x=y (c, f and vars ' + 'optional), if APP_ENV include the action f then after the ' + 'action execution the interpreter is exited') + g.add_argument('-B', '--bpython', + default=False, + action='store_true', help= + 'use bpython (if installed) when running in interactive shell, ' + 'see -S above') + g.add_argument('-P', '--plain', + default=False, + action='store_true', help= + 'use plain Python shell when running in interactive shell, ' + 'see -S above') + g.add_argument('-M', '--import_models', '--import-models', + default=False, + action='store_true', help= + 'auto import model files when running in interactive shell ' + '(default is %(default)s), see -S above. NOTE: when the APP_ENV ' + 'argument of -S include a controller c automatic import of ' + 'models is always enabled') + g.add_argument('-R', '--run', + type=existing_file, + metavar='PYTHON_FILE', help= + 'run PYTHON_FILE in web2py environment; require -S') + g.add_argument('-A', '--args', + default=[], + nargs=argparse.REMAINDER, help= + 'use this to pass arguments to the PYTHON_FILE above; require ' + '-R. NOTE: must be the last option because eat all remaining ' + 'arguments') + + # web server options + g = parser.add_argument_group('web server options') + g.add_argument('-s', '--server_name', '--server-name', + default=socket.gethostname(), + help='web server name (%(default)s)') + def ip_addr(v): + if not is_valid_ip_address(v): + raise argparse.ArgumentTypeError("bad IP address %s" % v) + return v + g.add_argument('-i', '--ip', + default='127.0.0.1', + type=ip_addr, metavar='IP_ADDR', help= + 'IP address of the server (%(default)s), accept either IPv4 or ' + 'IPv6 (e.g. ::1) addresses. NOTE: this option is ignored if ' + '--interface is specified') + def not_negative_int(v, err_label='value'): + try: + iv = int(v) + if iv < 0: raise ValueError() + return iv + except ValueError: + pass + raise argparse.ArgumentTypeError("bad %s %s" % (err_label, v)) + def port(v): + return not_negative_int(v, err_label='port') + g.add_argument('-p', '--port', + default=8000, + type=port, metavar='NUM', help= + 'port of server (%(default)d). ' + 'NOTE: this option is ignored if --interface is specified') + g.add_argument('-k', '--server_key', '--server-key', + '--ssl_private_key', # deprecated + type=existing_file, + metavar='FILE', help='server private key') + g.add_argument('-c', '--server_cert', '--server-cert', + '--ssl_certificate', # deprecated + type=existing_file, + metavar='FILE', help='server certificate') + g.add_argument('--ca_cert', '--ca-cert', + type=existing_file, + metavar='FILE', help='CA certificate') + def iface(v, sep=','): + if not v: + raise argparse.ArgumentTypeError('empty argument') + if sep == ':': + # deprecated --interfaces ip:port:key:cert:ca_cert + # IPv6 addresses in square brackets + if v.startswith('['): + # IPv6 + ip, v_remainder = v.split(']', 1) + ip = ip[1:] + ifp = v_remainder[1:].split(':') + ifp.insert(0, ip) + else: + # IPv4 + ifp = v.split(':') + else: + # --interface + ifp = v.split(sep, 5) + if not len(ifp) in (2, 4, 5): + raise argparse.ArgumentTypeError("bad interface %r" % v) + try: + ip_addr(ifp[0]) + ifp[1] = port(ifp[1]) + for fv in ifp[2:]: + existing_file(fv) + except argparse.ArgumentTypeError as ex: + raise argparse.ArgumentTypeError("bad interface %r (%s)" % (v, ex)) + return tuple(ifp) + g.add_argument('--interface', dest='interfaces', + default=[], action=ExtendAction, + type=iface, nargs='+', + metavar='IF_INFO', help= + 'listen on specified interface, IF_INFO = ' + 'IP_ADDR,PORT[,KEY_FILE,CERT_FILE[,CA_CERT_FILE]].' + ' NOTE: this option can be used multiple times to provide additional ' + 'interfaces to choose from but you can choose which one to listen to ' + 'only using the GUI otherwise the first interface specified is used') + def ifaces(v): + # deprecated --interfaces 'if1;if2;...' + if not v: + raise argparse.ArgumentTypeError('empty argument') + return [iface(i, ':') for i in v.split(';')] + g.add_argument('--interfaces', # deprecated + default=argparse.SUPPRESS, # do not set if absent + action=ExtendAction, + type=ifaces, + help=argparse.SUPPRESS) # do not show on help + g.add_argument('-d', '--pid_filename', '--pid-filename', + default='httpserver.pid', + metavar='FILE', help='server pid file (%(default)s)') + g.add_argument('-l', '--log_filename', '--log-filename', + default='httpserver.log', + metavar='FILE', help='server log file (%(default)s)') + g.add_argument('--min_threads', '--min-threads', + '--minthreads', '-n', '--numthreads', # deprecated + type=not_negative_int, metavar='NUM', + help='minimum number of server threads') + g.add_argument('--max_threads', '--max-threads', + '--maxthreads', # deprecated + type=not_negative_int, metavar='NUM', + help='maximum number of server threads') + g.add_argument('-q', '--request_queue_size', '--request-queue-size', + default=5, + type=not_negative_int, metavar='NUM', help= + 'max number of queued requests when server busy (%(default)d)') + g.add_argument('-o', '--timeout', + default=10, + type=not_negative_int, metavar='SECONDS', + help='timeout for individual request (%(default)d seconds)') + g.add_argument('--socket_timeout', '--socket-timeout', + default=5, + type=not_negative_int, metavar='SECONDS', + help='timeout for socket (%(default)d seconds)') + g.add_argument('-z', '--shutdown_timeout', # deprecated + type=not_negative_int, + help=argparse.SUPPRESS) # do not show on help + g.add_argument('-F', '--profiler_dir', '--profiler-dir', + '--profiler', # deprecated + help='profiler directory') + + # scheduler options + g = parser.add_argument_group('scheduler options') + g.add_argument('-X', '--with_scheduler', '--with-scheduler', + default=False, + action='store_true', help= + 'run schedulers alongside web server; require --K') + def is_app(app): + return is_appdir(folder, app) + def scheduler(v): + if not v: + raise argparse.ArgumentTypeError('empty argument') + if ',' in v: + # legacy "app1,..." + vl = [n.strip() for n in v.split(',')] + return [scheduler(iv) for iv in vl] + vp = [n.strip() for n in v.split(':')] + app = vp[0] + if not app: + raise argparse.ArgumentTypeError('empty application') + if not is_app(app): + warn("argument -K/--scheduler: bad application %r, skipped" % app) + return None + return ':'.join(filter(None, vp)) + g.add_argument('-K', '--scheduler', dest='schedulers', + default=[], action=ExtendAction, + type=scheduler, nargs='+', + metavar='APP_INFO', help= + 'run scheduler for the specified application(s), APP_INFO = ' + 'APP_NAME[:GROUPS], that is an optional list of groups can follow ' + 'the application name (e.g. app:group1:group2); require a scheduler ' + "to be defined in the application's models. NOTE: this option can " + 'be used multiple times to add schedulers') + + # cron options + g = parser.add_argument_group('cron options') + g.add_argument('-Y', '--with_cron', '--with-cron', + '--run-cron', # deprecated + default=False, + action='store_true', help= + 'run cron service alongside web server') + def crontab(v): + if not v: + raise argparse.ArgumentTypeError('empty argument') + if not is_app(v): + warn("argument --crontab: bad application %r, skipped" % v) + return None + return v + g.add_argument('--crontab', dest='crontabs', + default=[], action=ExtendAction, + type=crontab, nargs='+', + metavar='APP_NAME', help= + 'tell cron to read the crontab for the specified application(s) ' + 'only, the default behaviour is to read the crontab for all of the ' + 'installed applications. NOTE: this option can be used multiple ' + 'times to build the list of crontabs to be processed by cron') + g.add_argument('--soft_cron', '--soft-cron', + '--softcron', # deprecated + default=False, + action='store_true', help= + 'use cron software emulation instead of separate cron process; ' + 'require -Y. NOTE: use of cron software emulation is strongly ' + 'discouraged') + g.add_argument('-C', '--cron_run', '--cron-run', + '--cron', # deprecated + default=False, + action='store_true', help= + 'trigger a cron run and exit; usually used when invoked ' + 'from a system (external) crontab') + g.add_argument('--cron_job', # NOTE: this is intended for internal use only + default=False, + action='store_true', + help=argparse.SUPPRESS) # do not show on help + + # test options + g = parser.add_argument_group('test options') + g.add_argument('-v', '--verbose', + default=False, + action='store_true', help='increase verbosity') + g.add_argument('-T', '--run_doctests', '--run-doctests', + '--test', # deprecated + metavar='APP_ENV', help= + 'run doctests in application environment. APP_ENV like a/c/f (c, f ' + 'optional)') + g.add_argument('--run_system_tests', '--run-system-tests', + default=False, + action='store_true', help='run web2py test suite') + g.add_argument('--with_coverage', '--with-coverage', + default=False, + action='store_true', help= + 'collect coverage data when used with --run_system_tests; ' + 'require Python 2.7+ and the coverage module installed') + + # other options + g = parser.add_argument_group('other options') + g.add_argument('-G', '--GAE', dest='gae', + metavar='APP_NAME', help= + 'will create app.yaml and gaehandler.py and exit') + + options = parse_args(parser, sys.argv[1:], + deprecated_opts, integer_log_level) + + # make a copy of all options for global_settings + copy_options = copy.deepcopy(options) + copy_options.password = '******' + global_settings.cmd_options = copy_options + + return options + + +REGEX_PEP263 = r'^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)' + +def get_pep263_encoding(source): + """ + Read python source file encoding, according to PEP 263, see + https://www.python.org/dev/peps/pep-0263/ + """ + with open(source, 'r') as sf: + l12 = (sf.readline(), sf.readline()) + m12 = re.match(REGEX_PEP263, l12[0]) or re.match(REGEX_PEP263, l12[1]) + return m12 and m12.group(1) + + +IGNORE = lambda: None + +def load_config(config_file, opt_map): + """ + Load options from config file (a Python script). + + config_file(str): file name + opt_map(dict): mapping fom option name (key) to callable (val), + used to post-process parsed value for the option + + Notice that the configuring Python script is never executed/imported, + instead the ast library is used to evaluate each option assignment, + provided that it is writen on a single line. + + Returns an OrderedDict with sourced options. + """ + REGEX_ASSIGN_EXP = re.compile(r'\s*=\s*(.+)') + map_items = opt_map.items() + # preserve the order of loaded options even if this is not needed + pl = OrderedDict() + config_encoding = get_pep263_encoding(config_file) + # NOTE: assume 'ascii' encoding when not explicitly stated (Python 2), + # this is not correct for Python 3 where the default is 'utf-8' + open_kwargs = dict() if PY2 else dict(encoding=config_encoding or 'ascii') + with open(config_file, 'r', **open_kwargs) as cfil: + for linenum, clin in enumerate(cfil, start=1): + if PY2 and config_encoding: + clin = unicode(clin, config_encoding) + clin = clin.strip() + for opt, mapr in map_items: + if clin.startswith(opt): + m = REGEX_ASSIGN_EXP.match(clin[len(opt):]) + if m is None: continue + try: + val = opt_map[opt](ast.literal_eval(m.group(1))) + except: + die("cannot parse config file %r at line %d" % (config_file, linenum)) + if val is not IGNORE: + pl[opt] = val + return pl + + +def parse_args(parser, cli_args, deprecated_opts, integer_log_level, + namespace=None): + + #print('PARSING ARGS:', cli_args) + del integer_log_level[:] + options = parser.parse_args(cli_args, namespace) + #print('PARSED OPTIONS:', options) + + # warn for deprecated options + deprecated_args = [a for a in cli_args if a in deprecated_opts] + for da in deprecated_args: + # verify if it was a real option by looking into + # parsed values for the actual destination + hint = deprecated_opts[da] + dest = (hint or da).lstrip('-') + default = parser.get_default(dest) + if da == '--interfaces': + hint = '--interface' + if getattr(options, dest) is not default: + # the option has been specified + msg = "%s is deprecated" % da + if hint: + msg += ", use %s instead" % hint + warn(msg) + # warn for deprecated values + if integer_log_level and '--debug' not in deprecated_args: + warn('integer argument for -D/--log_level is deprecated, ' + 'use label instead') + # fix schedulers and die if all were skipped + if None in options.schedulers: + options.schedulers = [i for i in options.schedulers if i is not None] + if not options.schedulers: + die('no scheduler left') + # fix crontabs and die if all were skipped + if None in options.crontabs: + options.crontabs = [i for i in options.crontabs if i is not None] + if not options.crontabs: + die('no crontab left') + # taskbar + if options.taskbar and os.name != 'nt': + warn('--taskbar not supported on this platform, skipped') + options.taskbar = False + # options consistency checkings + if options.run and not options.shell: + die('-R/--run requires -S/--shell', exit_status=2) + if options.args and not options.run: + die('-A/--args requires -R/--run', exit_status=2) + if options.with_scheduler and not options.schedulers: + die('-X/--with_scheduler requires -K/--scheduler', exit_status=2) + if options.soft_cron and not options.with_cron: + die('--soft_cron requires -Y/--with_cron', exit_status=2) + if options.shell: + for o, os in dict(with_scheduler='-X/--with_scheduler', + schedulers='-K/--scheduler', + with_cron='-Y/--with_cron', + cron_run='-C/--cron_run', + run_doctests='-T/--run_doctests', + run_system_tests='--run_system_tests').items(): + if getattr(options, o): + die("-S/--shell and %s are conflicting options" % os, + exit_status=2) + if options.bpython and options.plain: + die('-B/--bpython and -P/--plain are conflicting options', + exit_status=2) + if options.cron_run: + for o, os in dict(with_cron='-Y/--with_cron', + run_doctests='-T/--run_doctests', + run_system_tests='--run_system_tests').items(): + if getattr(options, o): + die("-C/--cron_run and %s are conflicting options" % os, + exit_status=2) + if options.run_doctests and options.run_system_tests: + die('-T/--run_doctests and --run_system_tests are conflicting options', + exit_status=2) + + if options.config: + # load options from file, + # all options sourced from file that evaluates to False + # are skipped, the special IGNORE value is used for this + store_true = lambda v: True if v else IGNORE + str_or_default = lambda v : str(v) if v else IGNORE + list_or_default = lambda v : ( + [str(i) for i in v] if isinstance(v, list) else [str(v)]) if v \ + else IGNORE + # NOTE: 'help', 'version', 'folder', 'cron_job' and 'GAE' are not + # sourced from file, the same applies to deprecated options + opt_map = { + # global options + 'config': str_or_default, + 'add_options': store_true, + 'password': str_or_default, + 'errors_to_console': store_true, + 'no_banner': store_true, + 'quiet': store_true, + 'log_level': str_or_default, + # GUI options + 'no_gui': store_true, + 'taskbar': store_true, + # console options + 'shell': str_or_default, + 'bpython': store_true, + 'plain': store_true, + 'import_models': store_true, + 'run': str_or_default, + 'args': list_or_default, + # web server options + 'server_name': str_or_default, + 'ip': str_or_default, + 'port': str_or_default, + 'server_key': str_or_default, + 'server_cert': str_or_default, + 'ca_cert': str_or_default, + 'interface': list_or_default, + 'pid_filename': str_or_default, + 'log_filename': str_or_default, + 'min_threads': str_or_default, + 'max_threads': str_or_default, + 'request_queue_size': str_or_default, + 'timeout': str_or_default, + 'socket_timeout': str_or_default, + 'profiler_dir': str_or_default, + # scheduler options + 'with_scheduler': store_true, + 'scheduler': list_or_default, + # cron options + 'with_cron': store_true, + 'crontab': list_or_default, + 'soft_cron': store_true, + 'cron_run': store_true, + # test options + 'verbose': store_true, + 'run_doctests': str_or_default, + 'run_system_tests': store_true, + 'with_coverage': store_true, + } + od = load_config(options.config, opt_map) + #print("LOADED FROM %s:" % options.config, od) + # convert loaded options dict as retuned by load_config + # into a list of arguments for further parsing by parse_args + file_args = []; args_args = [] # '--args' must be the last + for key, val in od.items(): + if key != 'args': + file_args.append('--' + key) + if isinstance(val, list): file_args.extend(val) + elif not isinstance(val, bool): file_args.append(val) + else: + args_args = ['--args'] + val + file_args += args_args + + if options.add_options: + # add options to existing ones, + # must clear config to avoid infinite recursion + options.config = options.add_options = None + return parse_args(parser, file_args, + deprecated_opts, integer_log_level, options) + return parse_args(parser, file_args, + deprecated_opts, integer_log_level) + + return options diff --git a/gluon/fileutils.py b/gluon/fileutils.py index 9424d32e..2d02bcff 100644 --- a/gluon/fileutils.py +++ b/gluon/fileutils.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- """ @@ -374,7 +373,7 @@ def get_session(request, other_application='admin'): if not os.path.exists(session_filename): session_filename = generate(session_filename) osession = storage.load_storage(session_filename) - except: + except Exception: osession = storage.Storage() return osession diff --git a/gluon/globals.py b/gluon/globals.py index fc14ceff..9dcefe61 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- """ @@ -355,11 +354,9 @@ class Request(Storage): """ cmd_opts = global_settings.cmd_options # checking if this is called within the scheduler or within the shell - # in addition to checking if it's a cronjob - # FIXME: cmd_opts.scheduler does not imply that - # we are running in the scheduler - if (self.is_https or cmd_opts and ( - cmd_opts.shell or cmd_opts.scheduler or cmd_opts.cronjob)): + # in addition to checking if it's a cron job + if (self.is_https or self.is_scheduler or cmd_opts and ( + cmd_opts.shell or cmd_opts.cron_job)): current.session.secure() else: current.session.forget() diff --git a/gluon/main.py b/gluon/main.py index 68e4307d..c94825dc 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -1,4 +1,3 @@ -#!/bin/env python # -*- coding: utf-8 -*- """ @@ -557,7 +556,8 @@ def wsgibase(environ, responder): return wsgibase(new_environ, responder) if global_settings.web2py_crontype == 'soft': cmd_opts = global_settings.cmd_options - newcron.softcron(global_settings.applications_parent).start() + newcron.softcron(global_settings.applications_parent, + apps=cmd_opts and cmd_opts.crontabs).start() return http_response.to(responder, env=env) diff --git a/gluon/newcron.py b/gluon/newcron.py index 5eba24e0..b94f48fc 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -287,7 +287,7 @@ def crondance(applications_parent, ctype='soft', startup=False, apps=None): ('dom', now_s.tm_mday), ('dow', (now_s.tm_wday + 1) % 7)) - if apps is None: + if not apps: apps = [x for x in os.listdir(apppath) if os.path.isdir(os.path.join(apppath, x))] @@ -303,10 +303,7 @@ def crondance(applications_parent, ctype='soft', startup=False, apps=None): base_commands.append(w2p_path) if applications_parent != global_settings.gluon_parent: base_commands.extend(('-f', applications_parent)) - base_commands.extend(('--cronjob', '--no-banner', '--nogui', '--plain', - # FIXME: this should not be needed since we are - # not launching the web server - '-a', '""')) + base_commands.extend(('--cron_job', '--no_banner', '--no_gui', '--plain')) for app in apps: if _cron_stopping: diff --git a/gluon/packages/yatl b/gluon/packages/yatl index 8c6f1e1f..3fb9abba 160000 --- a/gluon/packages/yatl +++ b/gluon/packages/yatl @@ -1 +1 @@ -Subproject commit 8c6f1e1f17f08d5638490fb3dfe736953f585f84 +Subproject commit 3fb9abbac896a8c00ce102b3d0e0503638bb7fd9 diff --git a/gluon/shell.py b/gluon/shell.py index 89be48dc..889035ff 100644 --- a/gluon/shell.py +++ b/gluon/shell.py @@ -146,7 +146,7 @@ def env( request.is_shell = cmd_opts.shell is not None else: ip = '127.0.0.1'; port = 8000 - # FIXME: what about request.is_shell ? + request.is_shell = False request.is_scheduler = False request.env.http_host = '%s:%s' % (ip, port) request.env.remote_addr = '127.0.0.1' @@ -213,7 +213,7 @@ def run( startfile=None, bpython=False, python_code=None, - cronjob=False, + cron_job=False, scheduler_job=False): """ Start interactive shell or run Python script (startfile) in web2py @@ -233,7 +233,7 @@ def run( adir = os.path.join('applications', a) if not os.path.exists(adir): - if not cronjob and not scheduler_job and \ + if not cron_job and not scheduler_job and \ sys.stdin and not sys.stdin.name == '/dev/null': confirm = raw_input( 'application %s does not exist, create (y/n)?' % a) @@ -259,7 +259,7 @@ def run( pyfile = os.path.join('applications', a, 'controllers', c + '.py') pycfile = os.path.join('applications', a, 'compiled', "controllers_%s_%s.pyc" % (c, f)) - if ((cronjob and os.path.isfile(pycfile)) + if ((cron_job and os.path.isfile(pycfile)) or not os.path.isfile(pyfile)): exec(read_pyc(pycfile), _env) elif os.path.isfile(pyfile): @@ -380,7 +380,7 @@ def test(testpath, import_models=True, verbose=False): import doctest if os.path.isfile(testpath): - mo = re.match(r'(|.*/)applications/(?P[^/]+)', testpath) + mo = re.search('/?applications/(?P[^/]+)', testpath) if not mo: die('test file is not in application directory: %s' % testpath) diff --git a/gluon/tests/test_scheduler.py b/gluon/tests/test_scheduler.py index 06fc50fb..aecbcb5a 100644 --- a/gluon/tests/test_scheduler.py +++ b/gluon/tests/test_scheduler.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- """ @@ -636,7 +635,7 @@ def termination(): def exec_sched(self): import subprocess - call_args = [sys.executable, 'web2py.py', '--no-banner', '-D', '20','-K', 'welcome'] + call_args = [sys.executable, 'web2py.py', '--no_banner', '-D', 'INFO','-K', 'welcome'] ret = subprocess.call(call_args, env=dict(os.environ)) return ret diff --git a/gluon/widget.py b/gluon/widget.py index 37d1e730..5b6d909a 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -5,8 +5,8 @@ | Copyrighted by Massimo Di Pierro | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) -The widget is called from web2py ----------------------------------- +GUI widget and services start function +-------------------------------------- """ from __future__ import print_function @@ -16,15 +16,15 @@ from gluon._compat import thread, xrange, PY2 import time import threading import os -import copy import socket import signal import math import logging import getpass -from gluon import main, newcron -from gluon.fileutils import read_file, write_file, create_welcome_w2p +from gluon import main, newcron +from gluon.fileutils import read_file, create_welcome_w2p +from gluon.console import console from gluon.settings import global_settings from gluon.shell import die, run, test from gluon.utils import is_valid_ip_address, is_loopback_ip_address, getipaddrinfo @@ -314,22 +314,20 @@ class web2pyDialog(object): self.tb = None def update_schedulers(self, start=False): - applications_folder = os.path.join(self.options.folder, 'applications') - apps = [] - available_apps = [ - arq for arq in os.listdir(applications_folder) - if os.path.isdir(os.path.join(applications_folder, arq)) - ] - if start: - # the widget takes care of starting the scheduler - if self.options.scheduler and self.options.with_scheduler: - apps = [app for app - in map(lambda ag : ag.split(':', 1)[0].strip(), self.options.scheduler.split(',')) - if app in available_apps] + if start and self.options.with_scheduler and self.options.with_schedulers: + # the widget takes care of starting the schedulers + apps = [ag.split(':', 1)[0] for ag in self.options.with_schedulers] + else: + apps = [] for app in apps: self.try_start_scheduler(app) # reset the menu + applications_folder = os.path.join(self.options.folder, 'applications') + available_apps = [ + arq for arq in os.listdir(applications_folder) + if os.path.isdir(os.path.join(applications_folder, arq)) + ] self.schedmenu.delete(0, len(available_apps)) for arq in available_apps: @@ -351,7 +349,7 @@ class web2pyDialog(object): code = "from gluon.globals import current;current._scheduler.loop()" print('starting scheduler from widget for "%s"...' % app) args = (app, True, True, None, False, code, False, True) - logging.getLogger().setLevel(self.options.debuglevel) + logging.getLogger().setLevel(self.options.log_level) p = Process(target=run, args=args) self.scheduler_processes[app] = p self.update_schedulers() @@ -473,7 +471,7 @@ class web2pyDialog(object): except: return self.error('invalid port number') - if self.options.ssl_certificate and self.options.ssl_private_key: + if self.options.server_key and self.options.server_cert: proto = 'https' else: proto = 'http' @@ -492,11 +490,11 @@ class web2pyDialog(object): pid_filename=options.pid_filename, log_filename=options.log_filename, profiler_dir=options.profiler_dir, - ssl_certificate=options.ssl_certificate, - ssl_private_key=options.ssl_private_key, + ssl_certificate=options.server_cert, + ssl_private_key=options.server_key, ssl_ca_certificate=options.ca_cert, - min_threads=options.minthreads, - max_threads=options.maxthreads, + min_threads=options.min_threads, + max_threads=options.max_threads, server_name=options.server_name, request_queue_size=req_queue_size, timeout=options.timeout, @@ -582,321 +580,6 @@ class web2pyDialog(object): self.canvas.after(1000, self.update_canvas) -def console(): - """ Defines the behavior of the console web2py execution """ - import optparse - - parser = optparse.OptionParser( - usage='python %prog [options]', - version=ProgramVersion, - description='web2py Web Framework startup script.', - epilog='''NOTE: unless a password is specified (-a 'passwd') -web2py will attempt to run a GUI to ask for it when starting the web server -(if not disabled with --nogui).''') - - parser.add_option('-i', '--ip', - default='127.0.0.1', - metavar='IP_ADDR', help=\ - 'IP address of the server (e.g., 127.0.0.1 or ::1); ' \ - 'Note: This value is ignored when using the --interfaces option') - - parser.add_option('-p', '--port', - default=8000, - type='int', metavar='NUM', help=\ - 'port of server (%default); ' \ - 'Note: This value is ignored when using the --interfaces option') - - parser.add_option('-G', '--GAE', dest='gae', - default=None, - metavar='APP_NAME', help=\ - 'will create app.yaml and gaehandler.py and exit') - - parser.add_option('-a', '--password', - default='', - help=\ - 'password to be used for administration ' \ - '(use "" to reuse the last password), ' \ - 'when no password is available the administrative ' \ - 'interface will be disabled') - - parser.add_option('-c', '--ssl_certificate', - default=None, - metavar='FILE', help='server certificate file') - - parser.add_option('-k', '--ssl_private_key', - default=None, - metavar='FILE', help='server private key file') - - parser.add_option('--ca-cert', dest='ca_cert', # not needed - default=None, - metavar='FILE', help='CA certificate file') - - parser.add_option('-d', '--pid_filename', - default='httpserver.pid', - metavar='FILE', help='server pid file (%default)') - - parser.add_option('-l', '--log_filename', - default='httpserver.log', - metavar='FILE', help='server log file (%default)') - - parser.add_option('-n', '--numthreads', - default=None, - type='int', metavar='NUM', - help='number of threads (deprecated)') - - parser.add_option('--minthreads', - default=None, - type='int', metavar='NUM', - help='minimum number of server threads') - - parser.add_option('--maxthreads', - default=None, - type='int', metavar='NUM', - help='maximum number of server threads') - - parser.add_option('-s', '--server_name', - default=socket.gethostname(), - help='web server name (%default)') - - parser.add_option('-q', '--request_queue_size', - default=5, - type='int', metavar='NUM', - help=\ - 'max number of queued requests when server unavailable (%default)') - - parser.add_option('-o', '--timeout', - default=10, - type='int', metavar='SECONDS', - help='timeout for individual request (%default seconds)') - - parser.add_option('-z', '--shutdown_timeout', - default=None, - type='int', metavar='SECONDS', - help=\ - 'timeout on server shutdown; this value is not used by ' \ - 'Rocket web server') - - parser.add_option('--socket-timeout', dest='socket_timeout', # not needed - default=5, - type='int', metavar='SECONDS', - help='timeout for socket (%default seconds)') - - parser.add_option('-f', '--folder', - default=os.getcwd(), metavar='WEB2PY_DIR', - help='folder from which to run web2py') - - parser.add_option('-v', '--verbose', - default=False, - action='store_true', - help='increase --test and --run_system_tests verbosity') - - parser.add_option('-Q', '--quiet', - default=False, - action='store_true', - help='disable all output') - - parser.add_option('-e', '--errors_to_console', - default=False, - action='store_true', - help='log all errors to console') - - parser.add_option('-D', '--debug', dest='debuglevel', - default=30, - type='int', - metavar='LOG_LEVEL', help=\ - 'set log level (0-100, 0 means all, 100 means none; ' \ - 'default is %default)') - - parser.add_option('-S', '--shell', - default=None, - metavar='APPNAME', help=\ - 'run web2py in interactive shell or IPython (if installed) with ' \ - 'specified appname (if app does not exist it will be created). ' \ - 'APPNAME like a/c/f?x=y (c, f and vars optional)') - - parser.add_option('-B', '--bpython', - default=False, - action='store_true', - help=\ - 'run web2py in interactive shell or bpython (if installed) with ' \ - 'specified appname (if app does not exist it will be created). ' \ - 'Use combined with --shell') - - parser.add_option('-P', '--plain', - default=False, - action='store_true', - help=\ - 'only use plain python shell; should be used with --shell option') - - parser.add_option('-M', '--import_models', - default=False, - action='store_true', - help=\ - 'auto import model files (default is %default); should be used ' \ - 'with --shell option') - - parser.add_option('-R', '--run', - default='', # NOTE: used for sys.argv[0] if --shell - metavar='PYTHON_FILE', help=\ - 'run PYTHON_FILE in web2py environment; ' \ - 'should be used with --shell option') - - parser.add_option('-K', '--scheduler', - default=None, - metavar='APP_LIST', help=\ - 'run scheduled tasks for the specified apps: expects a list of ' \ - 'app names as app1,app2,app3 ' \ - 'or a list of app:groups as app1:group1:group2,app2:group1 ' \ - '(only strings, no spaces allowed). NOTE: ' \ - 'Requires a scheduler defined in the models') - - parser.add_option('-X', '--with-scheduler', dest='with_scheduler', # not needed - default=False, - action='store_true', - help=\ - 'run schedulers alongside webserver, needs -K') - - parser.add_option('-T', '--test', - default=None, - metavar='TEST_PATH', help=\ - 'run doctests in web2py environment; ' \ - 'TEST_PATH like a/c/f (c, f optional)') - - parser.add_option('-C', '--cron', dest='extcron', - default=False, - action='store_true', - help=\ - 'trigger a cron run and exit; usually used when invoked ' \ - 'from a system crontab') - - parser.add_option('--softcron', - default=False, - action='store_true', - help=\ - 'use software cron emulation instead of separate cron process, '\ - 'needs -Y; NOTE: use of software cron emulation is strongly ' - 'discouraged') - - parser.add_option('-Y', '--run-cron', dest='runcron', - default=False, - action='store_true', - help='start the background cron process') - - parser.add_option('-J', '--cronjob', - default=False, - action='store_true', - # NOTE: help suppressed because this option is - # intended for internal use only - help=optparse.SUPPRESS_HELP) - - parser.add_option('-L', '--config', - default='', - help='config file') - - parser.add_option('-F', '--profiler', dest='profiler_dir', - default=None, - help='profiler dir') - - parser.add_option('-t', '--taskbar', - default=False, - action='store_true', - help='use web2py GUI and run in taskbar (system tray)') - - parser.add_option('--nogui', - default=False, - action='store_true', - help='do not run GUI') - - parser.add_option('-A', '--args', - default=None, - help=\ - 'should be followed by a list of arguments to be passed to script, ' \ - 'to be used with -S; NOTE: must be the last option because eat all ' \ - 'remaining arguments') - - parser.add_option('--no-banner', dest='no_banner', # not needed - default=False, - action='store_true', - help='do not print header banner') - - parser.add_option('--interfaces', - default=None, - help=\ - 'listen on multiple addresses: ' \ - '"ip1:port1:key1:cert1:ca_cert1;ip2:port2:key2:cert2:ca_cert2;..." ' \ - '(:key:cert:ca_cert optional; no spaces; IPv6 addresses must be in ' \ - 'square [] brackets)') - - parser.add_option('--run_system_tests', - default=False, - action='store_true', - help='run web2py tests') - - parser.add_option('--with_coverage', - default=False, - action='store_true', - help=\ - 'collect coverage data when used with --run_system_tests; ' \ - 'require Python 2.7+ and the coverage module installed') - - if '-A' in sys.argv: - k = sys.argv.index('-A') - elif '--args' in sys.argv: - k = sys.argv.index('--args') - else: - k = len(sys.argv) - sys.argv, other_args = sys.argv[:k], sys.argv[k + 1:] - (options, args) = parser.parse_args() - # TODO: warn or error if args (should be no unparsed arguments) - options.args = other_args - - if options.taskbar and os.name != 'nt': - # TODO: warn and disable taskbar instead of exit - die('taskbar not supported on this platform') - - if options.config.endswith('.py'): - options.config = options.config[:-3] - if options.config: - # import options from options.config file - try: - # FIXME: avoid __import__ - options2 = __import__(options.config) - except: - die("cannot import config file %s" % options.config) - for key in dir(options2): - if hasattr(options, key): - setattr(options, key, getattr(options2, key)) - - # transform options.interfaces, in the form - # "ip1:port1:key1:cert1:ca_cert1;[ip2]:port2;ip3:port3:key3:cert3" - # (no spaces; optional key:cert:ca_cert indicate SSL), into - # a list of tuples - if options.interfaces: - interfaces = options.interfaces.split(';') - options.interfaces = [] - for interface in interfaces: - if interface.startswith('['): - # IPv6 - ip, if_remainder = interface.split(']', 1) - ip = ip[1:] - interface = if_remainder[1:].split(':') - interface.insert(0, ip) - else: - # IPv4 - interface = interface.split(':') - interface[1] = int(interface[1]) # numeric port - options.interfaces.append(tuple(interface)) - - if options.numthreads is not None and options.minthreads is None: - options.minthreads = options.numthreads # legacy - - copy_options = copy.deepcopy(options) - copy_options.password = '******' - global_settings.cmd_options = copy_options - - return options, args - - def check_existent_app(options, appname): if os.path.isdir(os.path.join(options.folder, 'applications', appname)): return True @@ -921,11 +604,10 @@ def start_schedulers(options): except: sys.stderr.write('Sorry, -K only supported for Python 2.6+\n') return - logging.getLogger().setLevel(options.debuglevel) + logging.getLogger().setLevel(options.log_level) - apps = [[n.strip() for n in sched_app.split(':')] - for sched_app in options.scheduler.split(',')] - if len(apps) == 1 and not options.with_scheduler: + apps = [ag.split(':') for ag in options.schedulers] + if not options.with_scheduler and len(apps) == 1: app, code = get_code_for_scheduler(apps[0], options) if not app: return @@ -968,7 +650,7 @@ def start(): """ Starts server and other services """ # get command line arguments - (options, args) = console() + options = console(version=ProgramVersion) if options.gae: # write app.yaml, gaehandler.py, and exit @@ -990,7 +672,7 @@ def start(): return logger = logging.getLogger("web2py") - logger.setLevel(options.debuglevel) + logger.setLevel(options.log_level) # on new installation build the scaffolding app create_welcome_w2p() @@ -1027,36 +709,29 @@ def start(): from pydal.drivers import DRIVERS print('Database drivers available: %s' % ', '.join(DRIVERS)) - if options.test: + if options.run_doctests: # run doctests and exit - test(options.test, verbose=options.verbose) + test(options.run_doctests, verbose=options.verbose) return if options.shell: # run interactive shell and exit - sys.argv = [options.run] + options.args + sys.argv = [options.run or ''] + options.args run(options.shell, plain=options.plain, bpython=options.bpython, import_models=options.import_models, startfile=options.run, - cronjob=options.cronjob) + cron_job=options.cron_job) return - if options.extcron: + if options.cron_run: # run cron (extcron) and exit logger.debug('Starting extcron...') global_settings.web2py_crontype = 'external' - if options.scheduler: - # run cron for applications listed with --scheduler (-K) - apps = [app for app - in map(lambda ag : ag.split(':', 1)[0].strip(), options.scheduler.split(',')) - if check_existent_app(options, app)] - else: - apps = None - extcron = newcron.extcron(options.folder, apps=apps) + extcron = newcron.extcron(options.folder, apps=options.crontabs) extcron.start() extcron.join() return - if options.scheduler and not options.with_scheduler: + if not options.with_scheduler and options.schedulers: # run schedulers and exit try: start_schedulers(options) @@ -1064,22 +739,22 @@ def start(): pass return - if options.runcron: - if options.softcron: - print('Using softcron (but this is not very efficient)') + if options.with_cron: + if options.soft_cron: + print('Using cron software emulation (but this is not very efficient)') global_settings.web2py_crontype = 'soft' else: # start hardcron thread logger.debug('Starting hardcron...') global_settings.web2py_crontype = 'hard' - newcron.hardcron(options.folder).start() + newcron.hardcron(options.folder, apps=options.crontabs).start() # if no password provided and have Tk library start GUI (when not # explicitly disabled), we also need a GUI to put in taskbar (system tray) # when requested root = None - if (not options.nogui and options.password == '') or options.taskbar: + if (not options.no_gui and options.password == '') or options.taskbar: try: if PY2: import Tkinter as tkinter @@ -1089,10 +764,10 @@ def start(): except (ImportError, OSError): logger.warn( 'GUI not available because Tk library is not installed') - options.nogui = True + options.no_gui = True except: logger.exception('cannot get Tk root window, GUI disabled') - options.nogui = True + options.no_gui = True if root: # run GUI and exit @@ -1121,7 +796,7 @@ end tell spt = None - if options.scheduler and options.with_scheduler: + if options.with_scheduler and options.schedulers: # start schedulers in a separate thread spt = threading.Thread(target=start_schedulers, args=(options,)) spt.start() @@ -1144,7 +819,7 @@ end tell ip = first_if[0] port = first_if[1] - if options.ssl_certificate and options.ssl_private_key: + if options.server_key and options.server_cert: proto = 'https' else: proto = 'http' @@ -1186,11 +861,11 @@ end tell pid_filename=options.pid_filename, log_filename=options.log_filename, profiler_dir=options.profiler_dir, - ssl_certificate=options.ssl_certificate, - ssl_private_key=options.ssl_private_key, + ssl_certificate=options.server_cert, + ssl_private_key=options.server_key, ssl_ca_certificate=options.ca_cert, - min_threads=options.minthreads, - max_threads=options.maxthreads, + min_threads=options.min_threads, + max_threads=options.max_threads, server_name=options.server_name, request_queue_size=options.request_queue_size, timeout=options.timeout, diff --git a/scripts/web2py.fedora.sh b/scripts/web2py.fedora.sh index bb3a2534..6a334565 100644 --- a/scripts/web2py.fedora.sh +++ b/scripts/web2py.fedora.sh @@ -29,7 +29,7 @@ cd $DAEMON_DIR start() { echo -n $"Starting $DESC ($NAME): " - daemon --check $NAME $PYTHON $DAEMON_DIR/web2py.py -Q --nogui -a $ADMINPASS -d $PIDFILE -p $PORT & + daemon --check $NAME $PYTHON $DAEMON_DIR/web2py.py -Q --no_gui -a $ADMINPASS -d $PIDFILE -p $PORT & RETVAL=$? if [ $RETVAL -eq 0 ]; then touch /var/lock/subsys/$NAME