wsgidav.server.server_cli.py¶

  1"""
  2server_cli
  3==========
  4
  5:Author: Martin Wendt
  6:Copyright: Licensed under the MIT license, see LICENSE file in this package.
  7
  8Standalone server that runs WsgiDAV.
  9
 10These tasks are performed:
 11
 12    - Set up the configuration from defaults, configuration file, and command line
 13      options.
 14    - Instantiate the WsgiDAVApp object (which is a WSGI application)
 15    - Start a WSGI server for this WsgiDAVApp object
 16
 17Configuration is defined like this:
 18
 19    1. Get the name of a configuration file from command line option
 20       ``--config-file=FILENAME`` (or short ``-cFILENAME``).
 21       If this option is omitted, we use ``wsgidav.yaml`` in the current
 22       directory.
 23    2. Set reasonable default settings.
 24    3. If configuration file exists: read and use it to overwrite defaults.
 25    4. If command line options are passed, use them to override settings:
 26
 27       ``--host`` option overrides ``hostname`` setting.
 28
 29       ``--port`` option overrides ``port`` setting.
 30
 31       ``--root=FOLDER`` option creates a FilesystemProvider that publishes
 32       FOLDER on the '/' share.
 33"""
 34
 35import argparse
 36import copy
 37import logging
 38import os
 39import platform
 40import sys
 41import webbrowser
 42from pprint import pformat
 43from threading import Timer
 44
 45import yaml
 46
 47from wsgidav import __version__, util
 48from wsgidav.default_conf import DEFAULT_CONFIG, DEFAULT_VERBOSE
 49from wsgidav.fs_dav_provider import FilesystemProvider
 50from wsgidav.wsgidav_app import WsgiDAVApp
 51from wsgidav.xml_tools import use_lxml
 52
 53try:
 54    # Try pyjson5 first because it's faster than json5
 55    from pyjson5 import load as json_load
 56except ImportError:
 57    from json5 import load as json_load
 58
 59
 60__docformat__ = "reStructuredText"
 61
 62#: Try this config files if no --config=... option is specified
 63DEFAULT_CONFIG_FILES = ("wsgidav.yaml", "wsgidav.json")
 64
 65_logger = logging.getLogger("wsgidav")
 66
 67
 68def _get_common_info(config):
 69    """Calculate some common info."""
 70    # Support SSL
 71    ssl_certificate = util.fix_path(config.get("ssl_certificate"), config)
 72    ssl_private_key = util.fix_path(config.get("ssl_private_key"), config)
 73    ssl_certificate_chain = util.fix_path(config.get("ssl_certificate_chain"), config)
 74    ssl_adapter = config.get("ssl_adapter", "builtin")
 75    use_ssl = False
 76    if ssl_certificate and ssl_private_key:
 77        use_ssl = True
 78        # _logger.info("SSL / HTTPS enabled. Adapter: {}".format(ssl_adapter))
 79    elif ssl_certificate or ssl_private_key:
 80        raise RuntimeError(
 81            "Option 'ssl_certificate' and 'ssl_private_key' must be used together."
 82        )
 83
 84    protocol = "https" if use_ssl else "http"
 85    url = f"{protocol}://{config['host']}:{config['port']}"
 86    info = {
 87        "use_ssl": use_ssl,
 88        "ssl_cert": ssl_certificate,
 89        "ssl_pk": ssl_private_key,
 90        "ssl_adapter": ssl_adapter,
 91        "ssl_chain": ssl_certificate_chain,
 92        "protocol": protocol,
 93        "url": url,
 94    }
 95    return info
 96
 97
 98class FullExpandedPath(argparse.Action):
 99    """Expand user- and relative-paths"""
100
101    def __call__(self, parser, namespace, values, option_string=None):
102        new_val = os.path.abspath(os.path.expanduser(values))
103        setattr(namespace, self.dest, new_val)
104
105
106def _init_command_line_options():
107    """Parse command line options into a dictionary."""
108    description = """\
109
110Run a WEBDAV server to share file system folders.
111
112Examples:
113
114  Share filesystem folder '/temp' for anonymous access (no config file used):
115    wsgidav --port=80 --host=0.0.0.0 --root=/temp --auth=anonymous
116
117  Run using a specific configuration file:
118    wsgidav --port=80 --host=0.0.0.0 --config=~/my_wsgidav.yaml
119
120  If no config file is specified, the application will look for a file named
121  'wsgidav.yaml' in the current directory.
122  See
123    http://wsgidav.readthedocs.io/en/latest/run-configure.html
124  for some explanation of the configuration file format.
125  """
126
127    epilog = """\
128Licensed under the MIT license.
129See https://github.com/mar10/wsgidav for additional information.
130
131"""
132
133    parser = argparse.ArgumentParser(
134        prog="wsgidav",
135        description=description,
136        epilog=epilog,
137        allow_abbrev=False,
138        formatter_class=argparse.RawTextHelpFormatter,
139    )
140    parser.add_argument(
141        "-p",
142        "--port",
143        type=int,
144        # default=8080,
145        help="port to serve on (default: 8080)",
146    )
147    parser.add_argument(
148        "-H",  # '-h' conflicts with --help
149        "--host",
150        help=(
151            "host to serve from (default: localhost). 'localhost' is only "
152            "accessible from the local computer. Use 0.0.0.0 to make your "
153            "application public"
154        ),
155    )
156    parser.add_argument(
157        "-r",
158        "--root",
159        dest="root_path",
160        action=FullExpandedPath,
161        help="path to a file system folder to publish for RW as share '/'.",
162    )
163    parser.add_argument(
164        "--auth",
165        choices=("anonymous", "nt", "pam-login"),
166        help="quick configuration of a domain controller when no config file "
167        "is used",
168    )
169    parser.add_argument(
170        "--server",
171        choices=SUPPORTED_SERVERS.keys(),
172        # default="cheroot",
173        help="type of pre-installed WSGI server to use (default: cheroot).",
174    )
175    parser.add_argument(
176        "--ssl-adapter",
177        choices=("builtin", "pyopenssl"),
178        # default="builtin",
179        help="used by 'cheroot' server if SSL certificates are configured "
180        "(default: builtin).",
181    )
182
183    qv_group = parser.add_mutually_exclusive_group()
184    qv_group.add_argument(
185        "-v",
186        "--verbose",
187        action="count",
188        default=3,
189        help="increment verbosity by one (default: %(default)s, range: 0..5)",
190    )
191    qv_group.add_argument(
192        "-q", "--quiet", default=0, action="count", help="decrement verbosity by one"
193    )
194
195    qv_group = parser.add_mutually_exclusive_group()
196    qv_group.add_argument(
197        "-c",
198        "--config",
199        dest="config_file",
200        action=FullExpandedPath,
201        help=(
202            f"configuration file (default: {DEFAULT_CONFIG_FILES} in current directory)"
203        ),
204    )
205
206    qv_group.add_argument(
207        "--no-config",
208        action="store_true",
209        help=f"do not try to load default {DEFAULT_CONFIG_FILES}",
210    )
211
212    parser.add_argument(
213        "--browse",
214        action="store_true",
215        help="open browser on start",
216    )
217
218    parser.add_argument(
219        "-V",
220        "--version",
221        action="store_true",
222        help="print version info and exit (may be combined with --verbose)",
223    )
224
225    args = parser.parse_args()
226
227    args.verbose -= args.quiet
228    del args.quiet
229
230    if args.root_path and not os.path.isdir(args.root_path):
231        msg = f"{args.root_path} is not a directory"
232        parser.error(msg)
233
234    if args.version:
235        if args.verbose >= 4:
236            version_info = "WsgiDAV/{} {}/{}({} bit) {}".format(
237                __version__,
238                platform.python_implementation(),
239                util.PYTHON_VERSION,
240                "64" if sys.maxsize > 2**32 else "32",
241                platform.platform(aliased=True),
242            )
243            version_info += f"\nPython from: {sys.executable}"
244        else:
245            version_info = f"{__version__}"
246        print(version_info)
247        sys.exit()
248
249    if args.no_config:
250        pass
251        # ... else ignore default config files
252    elif args.config_file is None:
253        # If --config was omitted, use default (if it exists)
254        for filename in DEFAULT_CONFIG_FILES:
255            defPath = os.path.abspath(filename)
256            if os.path.exists(defPath):
257                if args.verbose >= 3:
258                    print(f"Using default configuration file: {defPath}")
259                args.config_file = defPath
260                break
261    else:
262        # If --config was specified convert to absolute path and assert it exists
263        args.config_file = os.path.abspath(args.config_file)
264        if not os.path.isfile(args.config_file):
265            parser.error(
266                f"Could not find specified configuration file: {args.config_file}"
267            )
268
269    # Convert args object to dictionary
270    cmdLineOpts = args.__dict__.copy()
271    if args.verbose >= 5:
272        print("Command line args:")
273        for k, v in cmdLineOpts.items():
274            print(f"    {k:>12}: {v}")
275    return cmdLineOpts, parser
276
277
278def _read_config_file(config_file, _verbose):
279    """Read configuration file options into a dictionary."""
280
281    config_file = os.path.abspath(config_file)
282
283    if not os.path.exists(config_file):
284        raise RuntimeError(f"Couldn't open configuration file {config_file!r}.")
285
286    if config_file.endswith(".json"):
287        with open(config_file, encoding="utf-8-sig") as fp:
288            conf = json_load(fp)
289
290    elif config_file.endswith(".yaml"):
291        with open(config_file, encoding="utf-8-sig") as fp:
292            conf = yaml.safe_load(fp)
293
294    else:
295        raise RuntimeError(
296            f"Unsupported config file format (expected yaml or json): {config_file}"
297        )
298
299    conf["_config_file"] = config_file
300    conf["_config_root"] = os.path.dirname(config_file)
301    return conf
302
303
304def _init_config():
305    """Setup configuration dictionary from default, command line and configuration file."""
306    cli_opts, parser = _init_command_line_options()
307    cli_verbose = cli_opts["verbose"]
308
309    # Set config defaults
310    config = copy.deepcopy(DEFAULT_CONFIG)
311    config["_config_file"] = None
312    config["_config_root"] = os.getcwd()
313
314    # Configuration file overrides defaults
315    config_file = cli_opts.get("config_file")
316    if config_file:
317        file_opts = _read_config_file(config_file, cli_verbose)
318        util.deep_update(config, file_opts)
319        if cli_verbose != DEFAULT_VERBOSE and "verbose" in file_opts:
320            if cli_verbose >= 2:
321                print(
322                    "Config file defines 'verbose: {}' but is overridden by command line: {}.".format(
323                        file_opts["verbose"], cli_verbose
324                    )
325                )
326            config["verbose"] = cli_verbose
327    else:
328        if cli_verbose >= 2:
329            print("Running without configuration file.")
330
331    # Command line overrides file
332    if cli_opts.get("port"):
333        config["port"] = cli_opts.get("port")
334    if cli_opts.get("host"):
335        config["host"] = cli_opts.get("host")
336    if cli_opts.get("profile") is not None:
337        config["profile"] = True
338    if cli_opts.get("server") is not None:
339        config["server"] = cli_opts.get("server")
340    if cli_opts.get("ssl_adapter") is not None:
341        config["ssl_adapter"] = cli_opts.get("ssl_adapter")
342
343    # Command line overrides file only if -v or -q where passed:
344    if cli_opts.get("verbose") != DEFAULT_VERBOSE:
345        config["verbose"] = cli_opts.get("verbose")
346
347    if cli_opts.get("root_path"):
348        root_path = os.path.abspath(cli_opts.get("root_path"))
349        config["provider_mapping"]["/"] = FilesystemProvider(
350            root_path,
351            fs_opts=config.get("fs_dav_provider"),
352        )
353
354    if config["verbose"] >= 5:
355        # TODO: remove passwords from user_mapping
356        config_cleaned = util.purge_passwords(config)
357        print(
358            "Configuration({}):\n{}".format(
359                cli_opts["config_file"], pformat(config_cleaned)
360            )
361        )
362
363    if not config["provider_mapping"]:
364        parser.error("No DAV provider defined.")
365
366    # Quick-configuration of DomainController
367    auth = cli_opts.get("auth")
368    auth_conf = util.get_dict_value(config, "http_authenticator", as_dict=True)
369    if auth and auth_conf.get("domain_controller"):
370        parser.error(
371            "--auth option can only be used when no domain_controller is configured"
372        )
373
374    if auth == "anonymous":
375        if config["simple_dc"]["user_mapping"]:
376            parser.error(
377                "--auth=anonymous can only be used when no user_mapping is configured"
378            )
379        auth_conf.update(
380            {
381                "domain_controller": "wsgidav.dc.simple_dc.SimpleDomainController",
382                "accept_basic": True,
383                "accept_digest": True,
384                "default_to_digest": True,
385            }
386        )
387        config["simple_dc"]["user_mapping"] = {"*": True}
388    elif auth == "nt":
389        if config.get("nt_dc"):
390            parser.error(
391                "--auth=nt can only be used when no nt_dc settings are configured"
392            )
393        auth_conf.update(
394            {
395                "domain_controller": "wsgidav.dc.nt_dc.NTDomainController",
396                "accept_basic": True,
397                "accept_digest": False,
398                "default_to_digest": False,
399            }
400        )
401        config["nt_dc"] = {}
402    elif auth == "pam-login":
403        if config.get("pam_dc"):
404            parser.error(
405                "--auth=pam-login can only be used when no pam_dc settings are configured"
406            )
407        auth_conf.update(
408            {
409                "domain_controller": "wsgidav.dc.pam_dc.PAMDomainController",
410                "accept_basic": True,
411                "accept_digest": False,
412                "default_to_digest": False,
413            }
414        )
415        config["pam_dc"] = {"service": "login"}
416    # print(config)
417
418    # if cli_opts.get("reload"):
419    #     print("Installing paste.reloader.", file=sys.stderr)
420    #     from paste import reloader  # @UnresolvedImport
421
422    #     reloader.install()
423    #     if config_file:
424    #         # Add config file changes
425    #         reloader.watch_file(config_file)
426    #     # import pydevd
427    #     # pydevd.settrace()
428
429    if config["suppress_version_info"]:
430        util.public_wsgidav_info = "WsgiDAV"
431        util.public_python_info = f"Python/{sys.version_info[0]}"
432
433    return cli_opts, config
434
435
436def _run_cheroot(app, config, _server):
437    """Run WsgiDAV using cheroot.server (https://cheroot.cherrypy.dev/)."""
438    try:
439        from cheroot import server, wsgi
440    except ImportError:
441        _logger.exception("Could not import Cheroot (https://cheroot.cherrypy.dev/).")
442        _logger.error("Try `pip install cheroot`.")
443        return False
444
445    version = (
446        f"{util.public_wsgidav_info} {wsgi.Server.version} {util.public_python_info}"
447    )
448    # wsgi.Server.version = version
449
450    info = _get_common_info(config)
451
452    # Support SSL
453    if info["use_ssl"]:
454        ssl_adapter = info["ssl_adapter"]
455        ssl_adapter = server.get_ssl_adapter_class(ssl_adapter)
456        wsgi.Server.ssl_adapter = ssl_adapter(
457            info["ssl_cert"], info["ssl_pk"], info["ssl_chain"]
458        )
459        _logger.info(f"SSL / HTTPS enabled. Adapter: {ssl_adapter}")
460
461    _logger.info(f"Running {version}")
462    _logger.info(f"Serving on {info['url']} ...")
463
464    server_args = {
465        "bind_addr": (config["host"], config["port"]),
466        "wsgi_app": app,
467        "server_name": version,
468        # File Explorer needs lot of threads (see issue #149):
469        "numthreads": 50,  # TODO: still required?
470    }
471    # Override or add custom args
472    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
473    server_args.update(custom_args)
474
475    class PatchedServer(wsgi.Server):
476        STARTUP_NOTIFICATION_DELAY = 0.5
477
478        def serve(self, *args, **kwargs):
479            _logger.error("wsgi.Server.serve")
480            if startup_event and not startup_event.is_set():
481                Timer(self.STARTUP_NOTIFICATION_DELAY, startup_event.set).start()
482                _logger.error("wsgi.Server is ready")
483            return super().serve(*args, **kwargs)
484
485    # If the caller passed a startup event, monkey patch the server to set it
486    # when the request handler loop is entered
487    startup_event = config.get("startup_event")
488    if startup_event:
489        server = PatchedServer(**server_args)
490    else:
491        server = wsgi.Server(**server_args)
492
493    try:
494        server.start()
495    except KeyboardInterrupt:
496        _logger.warning("Caught Ctrl-C, shutting down...")
497    finally:
498        server.stop()
499
500    return
501
502
503def _run_ext_wsgiutils(app, config, _server):
504    """Run WsgiDAV using ext_wsgiutils_server from the wsgidav package."""
505    from wsgidav.server import ext_wsgiutils_server
506
507    _logger.warning(
508        "WARNING: This single threaded server (ext-wsgiutils) is not meant for production."
509    )
510    try:
511        ext_wsgiutils_server.serve(config, app)
512    except KeyboardInterrupt:
513        _logger.warning("Caught Ctrl-C, shutting down...")
514    return
515
516
517def _run_gevent(app, config, server):
518    """Run WsgiDAV using gevent if gevent (https://www.gevent.org).
519
520    See
521      https://github.com/gevent/gevent/blob/master/src/gevent/pywsgi.py#L1356
522      https://github.com/gevent/gevent/blob/master/src/gevent/server.py#L38
523    for more options.
524    """
525    try:
526        import gevent
527        import gevent.monkey
528        from gevent.pywsgi import WSGIServer
529    except ImportError:
530        _logger.exception("Could not import gevent (http://www.gevent.org).")
531        _logger.error("Try `pip install gevent`.")
532        return False
533
534    gevent.monkey.patch_all()
535
536    info = _get_common_info(config)
537    version = f"gevent/{gevent.__version__}"
538    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
539
540    # Override or add custom args
541    server_args = {
542        "wsgi_app": app,
543        "bind_addr": (config["host"], config["port"]),
544    }
545    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
546    server_args.update(custom_args)
547
548    if info["use_ssl"]:
549        dav_server = WSGIServer(
550            server_args["bind_addr"],
551            app,
552            keyfile=info["ssl_pk"],
553            certfile=info["ssl_cert"],
554            ca_certs=info["ssl_chain"],
555        )
556    else:
557        dav_server = WSGIServer(server_args["bind_addr"], app)
558
559    # If the caller passed a startup event, monkey patch the server to set it
560    # when the request handler loop is entered
561    startup_event = config.get("startup_event")
562    if startup_event:
563
564        def _patched_start():
565            dav_server.start_accepting = org_start  # undo the monkey patch
566            org_start()
567            _logger.info("gevent is ready")
568            startup_event.set()
569
570        org_start = dav_server.start_accepting
571        dav_server.start_accepting = _patched_start
572
573    _logger.info(f"Running {version}")
574    _logger.info(f"Serving on {info['url']} ...")
575    try:
576        gevent.spawn(dav_server.serve_forever())
577    except KeyboardInterrupt:
578        _logger.warning("Caught Ctrl-C, shutting down...")
579    return
580
581
582def _run_gunicorn(app, config, server):
583    """Run WsgiDAV using Gunicorn (https://gunicorn.org)."""
584    try:
585        import gunicorn.app.base
586    except ImportError:
587        _logger.exception("Could not import Gunicorn (https://gunicorn.org).")
588        _logger.error("Try `pip install gunicorn` (UNIX only).")
589        return False
590
591    info = _get_common_info(config)
592
593    class GunicornApplication(gunicorn.app.base.BaseApplication):
594        def __init__(self, app, options=None):
595            self.options = options or {}
596            self.application = app
597            super().__init__()
598
599        def load_config(self):
600            config = {
601                key: value
602                for key, value in self.options.items()
603                if key in self.cfg.settings and value is not None
604            }
605            for key, value in config.items():
606                self.cfg.set(key.lower(), value)
607
608        def load(self):
609            return self.application
610
611    # See https://docs.gunicorn.org/en/latest/settings.html
612    server_args = {
613        "bind": "{}:{}".format(config["host"], config["port"]),
614        "threads": 50,
615        "timeout": 1200,
616    }
617    if info["use_ssl"]:
618        server_args.update(
619            {
620                "keyfile": info["ssl_pk"],
621                "certfile": info["ssl_cert"],
622                "ca_certs": info["ssl_chain"],
623                # "ssl_version": ssl_version
624                # "cert_reqs": ssl_cert_reqs
625                # "ciphers": ssl_ciphers
626            }
627        )
628    # Override or add custom args
629    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
630    server_args.update(custom_args)
631
632    version = f"gunicorn/{gunicorn.__version__}"
633    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
634    _logger.info(f"Running {version} ...")
635
636    GunicornApplication(app, server_args).run()
637
638
639def _run_paste(app, config, server):
640    """Run WsgiDAV using paste.httpserver, if Paste is installed.
641
642    See http://pythonpaste.org/modules/httpserver.html for more options
643    """
644    try:
645        from paste import httpserver
646    except ImportError:
647        _logger.exception(
648            "Could not import paste.httpserver (https://github.com/cdent/paste)."
649        )
650        _logger.error("Try `pip install paste`.")
651        return False
652
653    info = _get_common_info(config)
654
655    version = httpserver.WSGIHandler.server_version
656    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
657
658    # See http://pythonpaste.org/modules/httpserver.html for more options
659    server = httpserver.serve(
660        app,
661        host=config["host"],
662        port=config["port"],
663        server_version=version,
664        # This option enables handling of keep-alive and expect-100:
665        protocol_version="HTTP/1.1",
666        start_loop=False,
667    )
668
669    if config["verbose"] >= 5:
670        __handle_one_request = server.RequestHandlerClass.handle_one_request
671
672        def handle_one_request(self):
673            __handle_one_request(self)
674            if self.close_connection == 1:
675                _logger.debug("HTTP Connection : close")
676            else:
677                _logger.debug("HTTP Connection : continue")
678
679        server.RequestHandlerClass.handle_one_request = handle_one_request
680
681    _logger.info(f"Running {version} ...")
682    host, port = server.server_address
683    if host == "0.0.0.0":
684        _logger.info(f"Serving on 0.0.0.0:{port} view at http://127.0.0.1:{port}")
685    else:
686        _logger.info(f"Serving on {info['url']}")
687
688    try:
689        server.serve_forever()
690    except KeyboardInterrupt:
691        _logger.warning("Caught Ctrl-C, shutting down...")
692    return
693
694
695def _run_uvicorn(app, config, server):
696    """Run WsgiDAV using Uvicorn (https://www.uvicorn.org)."""
697    try:
698        import uvicorn
699    except ImportError:
700        _logger.exception("Could not import Uvicorn (https://www.uvicorn.org).")
701        _logger.error("Try `pip install uvicorn`.")
702        return False
703
704    info = _get_common_info(config)
705
706    # See https://www.uvicorn.org/settings/
707    server_args = {
708        "interface": "wsgi",
709        "host": config["host"],
710        "port": config["port"],
711        # TODO: see _run_cheroot()
712    }
713    if info["use_ssl"]:
714        server_args.update(
715            {
716                "ssl_keyfile": info["ssl_pk"],
717                "ssl_certfile": info["ssl_cert"],
718                "ssl_ca_certs": info["ssl_chain"],
719                # "ssl_keyfile_password": ssl_keyfile_password
720                # "ssl_version": ssl_version
721                # "ssl_cert_reqs": ssl_cert_reqs
722                # "ssl_ciphers": ssl_ciphers
723            }
724        )
725    # Override or add custom args
726    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
727    server_args.update(custom_args)
728
729    version = f"uvicorn/{uvicorn.__version__}"
730    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
731    _logger.info(f"Running {version} ...")
732
733    uvicorn.run(app, **server_args)
734
735
736def _run_wsgiref(app, config, _server):
737    """Run WsgiDAV using wsgiref.simple_server (https://docs.python.org/3/library/wsgiref.html)."""
738    from wsgiref.simple_server import WSGIRequestHandler, make_server
739
740    version = WSGIRequestHandler.server_version
741    version = f"{util.public_wsgidav_info} {version}"  # {util.public_python_info}"
742    _logger.info(f"Running {version} ...")
743
744    _logger.warning(
745        "WARNING: This single threaded server (wsgiref) is not meant for production."
746    )
747    WSGIRequestHandler.server_version = version
748    httpd = make_server(config["host"], config["port"], app)
749    # httpd.RequestHandlerClass.server_version = version
750    try:
751        httpd.serve_forever()
752    except KeyboardInterrupt:
753        _logger.warning("Caught Ctrl-C, shutting down...")
754    return
755
756
757SUPPORTED_SERVERS = {
758    "cheroot": _run_cheroot,
759    "ext-wsgiutils": _run_ext_wsgiutils,
760    "gevent": _run_gevent,
761    "gunicorn": _run_gunicorn,
762    "paste": _run_paste,
763    "uvicorn": _run_uvicorn,
764    "wsgiref": _run_wsgiref,
765}
766
767
768def run():
769    cli_opts, config = _init_config()
770
771    # util.init_logging(config) # now handled in constructor:
772    config["logging"]["enable"] = True
773
774    info = _get_common_info(config)
775
776    app = WsgiDAVApp(config)
777
778    server = config["server"]
779    handler = SUPPORTED_SERVERS.get(server)
780    if not handler:
781        raise RuntimeError(
782            "Unsupported server type {!r} (expected {!r})".format(
783                server, "', '".join(SUPPORTED_SERVERS.keys())
784            )
785        )
786
787    if not use_lxml and config["verbose"] >= 3:
788        _logger.warning(
789            "Could not import lxml: using xml instead (up to 10% slower). "
790            "Consider `pip install lxml`(see https://pypi.python.org/pypi/lxml)."
791        )
792
793    if cli_opts["browse"]:
794        BROWSE_DELAY = 2.0
795
796        def _worker():
797            url = info["url"]
798            url = url.replace("0.0.0.0", "127.0.0.1")
799            _logger.info(f"Starting browser on {url} ...")
800            webbrowser.open(url)
801
802        Timer(BROWSE_DELAY, _worker).start()
803
804    handler(app, config, server)
805    return
806
807
808if __name__ == "__main__":
809    # Just in case...
810    from multiprocessing import freeze_support
811
812    freeze_support()
813
814    run()