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/{} Python/{}({} bit) {}".format(
237                __version__,
238                util.PYTHON_VERSION,
239                "64" if sys.maxsize > 2**32 else "32",
240                platform.platform(aliased=True),
241            )
242            version_info += f"\nPython from: {sys.executable}"
243        else:
244            version_info = f"{__version__}"
245        print(version_info)
246        sys.exit()
247
248    if args.no_config:
249        pass
250        # ... else ignore default config files
251    elif args.config_file is None:
252        # If --config was omitted, use default (if it exists)
253        for filename in DEFAULT_CONFIG_FILES:
254            defPath = os.path.abspath(filename)
255            if os.path.exists(defPath):
256                if args.verbose >= 3:
257                    print(f"Using default configuration file: {defPath}")
258                args.config_file = defPath
259                break
260    else:
261        # If --config was specified convert to absolute path and assert it exists
262        args.config_file = os.path.abspath(args.config_file)
263        if not os.path.isfile(args.config_file):
264            parser.error(
265                f"Could not find specified configuration file: {args.config_file}"
266            )
267
268    # Convert args object to dictionary
269    cmdLineOpts = args.__dict__.copy()
270    if args.verbose >= 5:
271        print("Command line args:")
272        for k, v in cmdLineOpts.items():
273            print(f"    {k:>12}: {v}")
274    return cmdLineOpts, parser
275
276
277def _read_config_file(config_file, _verbose):
278    """Read configuration file options into a dictionary."""
279
280    config_file = os.path.abspath(config_file)
281
282    if not os.path.exists(config_file):
283        raise RuntimeError(f"Couldn't open configuration file {config_file!r}.")
284
285    if config_file.endswith(".json"):
286        with open(config_file, encoding="utf-8-sig") as fp:
287            conf = json_load(fp)
288
289    elif config_file.endswith(".yaml"):
290        with open(config_file, encoding="utf-8-sig") as fp:
291            conf = yaml.safe_load(fp)
292
293    else:
294        raise RuntimeError(
295            f"Unsupported config file format (expected yaml or json): {config_file}"
296        )
297
298    conf["_config_file"] = config_file
299    conf["_config_root"] = os.path.dirname(config_file)
300    return conf
301
302
303def _init_config():
304    """Setup configuration dictionary from default, command line and configuration file."""
305    cli_opts, parser = _init_command_line_options()
306    cli_verbose = cli_opts["verbose"]
307
308    # Set config defaults
309    config = copy.deepcopy(DEFAULT_CONFIG)
310    config["_config_file"] = None
311    config["_config_root"] = os.getcwd()
312
313    # Configuration file overrides defaults
314    config_file = cli_opts.get("config_file")
315    if config_file:
316        file_opts = _read_config_file(config_file, cli_verbose)
317        util.deep_update(config, file_opts)
318        if cli_verbose != DEFAULT_VERBOSE and "verbose" in file_opts:
319            if cli_verbose >= 2:
320                print(
321                    "Config file defines 'verbose: {}' but is overridden by command line: {}.".format(
322                        file_opts["verbose"], cli_verbose
323                    )
324                )
325            config["verbose"] = cli_verbose
326    else:
327        if cli_verbose >= 2:
328            print("Running without configuration file.")
329
330    # Command line overrides file
331    if cli_opts.get("port"):
332        config["port"] = cli_opts.get("port")
333    if cli_opts.get("host"):
334        config["host"] = cli_opts.get("host")
335    if cli_opts.get("profile") is not None:
336        config["profile"] = True
337    if cli_opts.get("server") is not None:
338        config["server"] = cli_opts.get("server")
339    if cli_opts.get("ssl_adapter") is not None:
340        config["ssl_adapter"] = cli_opts.get("ssl_adapter")
341
342    # Command line overrides file only if -v or -q where passed:
343    if cli_opts.get("verbose") != DEFAULT_VERBOSE:
344        config["verbose"] = cli_opts.get("verbose")
345
346    if cli_opts.get("root_path"):
347        root_path = os.path.abspath(cli_opts.get("root_path"))
348        config["provider_mapping"]["/"] = FilesystemProvider(
349            root_path,
350            fs_opts=config.get("fs_dav_provider"),
351        )
352
353    if config["verbose"] >= 5:
354        # TODO: remove passwords from user_mapping
355        config_cleaned = util.purge_passwords(config)
356        print(
357            "Configuration({}):\n{}".format(
358                cli_opts["config_file"], pformat(config_cleaned)
359            )
360        )
361
362    if not config["provider_mapping"]:
363        parser.error("No DAV provider defined.")
364
365    # Quick-configuration of DomainController
366    auth = cli_opts.get("auth")
367    auth_conf = util.get_dict_value(config, "http_authenticator", as_dict=True)
368    if auth and auth_conf.get("domain_controller"):
369        parser.error(
370            "--auth option can only be used when no domain_controller is configured"
371        )
372
373    if auth == "anonymous":
374        if config["simple_dc"]["user_mapping"]:
375            parser.error(
376                "--auth=anonymous can only be used when no user_mapping is configured"
377            )
378        auth_conf.update(
379            {
380                "domain_controller": "wsgidav.dc.simple_dc.SimpleDomainController",
381                "accept_basic": True,
382                "accept_digest": True,
383                "default_to_digest": True,
384            }
385        )
386        config["simple_dc"]["user_mapping"] = {"*": True}
387    elif auth == "nt":
388        if config.get("nt_dc"):
389            parser.error(
390                "--auth=nt can only be used when no nt_dc settings are configured"
391            )
392        auth_conf.update(
393            {
394                "domain_controller": "wsgidav.dc.nt_dc.NTDomainController",
395                "accept_basic": True,
396                "accept_digest": False,
397                "default_to_digest": False,
398            }
399        )
400        config["nt_dc"] = {}
401    elif auth == "pam-login":
402        if config.get("pam_dc"):
403            parser.error(
404                "--auth=pam-login can only be used when no pam_dc settings are configured"
405            )
406        auth_conf.update(
407            {
408                "domain_controller": "wsgidav.dc.pam_dc.PAMDomainController",
409                "accept_basic": True,
410                "accept_digest": False,
411                "default_to_digest": False,
412            }
413        )
414        config["pam_dc"] = {"service": "login"}
415    # print(config)
416
417    # if cli_opts.get("reload"):
418    #     print("Installing paste.reloader.", file=sys.stderr)
419    #     from paste import reloader  # @UnresolvedImport
420
421    #     reloader.install()
422    #     if config_file:
423    #         # Add config file changes
424    #         reloader.watch_file(config_file)
425    #     # import pydevd
426    #     # pydevd.settrace()
427
428    if config["suppress_version_info"]:
429        util.public_wsgidav_info = "WsgiDAV"
430        util.public_python_info = f"Python/{sys.version_info[0]}"
431
432    return cli_opts, config
433
434
435def _run_cheroot(app, config, _server):
436    """Run WsgiDAV using cheroot.server (https://cheroot.cherrypy.dev/)."""
437    try:
438        from cheroot import server, wsgi
439    except ImportError:
440        _logger.exception("Could not import Cheroot (https://cheroot.cherrypy.dev/).")
441        _logger.error("Try `pip install cheroot`.")
442        return False
443
444    version = (
445        f"{util.public_wsgidav_info} {wsgi.Server.version} {util.public_python_info}"
446    )
447    # wsgi.Server.version = version
448
449    info = _get_common_info(config)
450
451    # Support SSL
452    if info["use_ssl"]:
453        ssl_adapter = info["ssl_adapter"]
454        ssl_adapter = server.get_ssl_adapter_class(ssl_adapter)
455        wsgi.Server.ssl_adapter = ssl_adapter(
456            info["ssl_cert"], info["ssl_pk"], info["ssl_chain"]
457        )
458        _logger.info(f"SSL / HTTPS enabled. Adapter: {ssl_adapter}")
459
460    _logger.info(f"Running {version}")
461    _logger.info(f"Serving on {info['url']} ...")
462
463    server_args = {
464        "bind_addr": (config["host"], config["port"]),
465        "wsgi_app": app,
466        "server_name": version,
467        # File Explorer needs lot of threads (see issue #149):
468        "numthreads": 50,  # TODO: still required?
469    }
470    # Override or add custom args
471    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
472    server_args.update(custom_args)
473
474    class PatchedServer(wsgi.Server):
475        STARTUP_NOTIFICATION_DELAY = 0.5
476
477        def serve(self, *args, **kwargs):
478            _logger.error("wsgi.Server.serve")
479            if startup_event and not startup_event.is_set():
480                Timer(self.STARTUP_NOTIFICATION_DELAY, startup_event.set).start()
481                _logger.error("wsgi.Server is ready")
482            return super().serve(*args, **kwargs)
483
484    # If the caller passed a startup event, monkey patch the server to set it
485    # when the request handler loop is entered
486    startup_event = config.get("startup_event")
487    if startup_event:
488        server = PatchedServer(**server_args)
489    else:
490        server = wsgi.Server(**server_args)
491
492    try:
493        server.start()
494    except KeyboardInterrupt:
495        _logger.warning("Caught Ctrl-C, shutting down...")
496    finally:
497        server.stop()
498
499    return
500
501
502def _run_ext_wsgiutils(app, config, _server):
503    """Run WsgiDAV using ext_wsgiutils_server from the wsgidav package."""
504    from wsgidav.server import ext_wsgiutils_server
505
506    _logger.warning(
507        "WARNING: This single threaded server (ext-wsgiutils) is not meant for production."
508    )
509    try:
510        ext_wsgiutils_server.serve(config, app)
511    except KeyboardInterrupt:
512        _logger.warning("Caught Ctrl-C, shutting down...")
513    return
514
515
516def _run_gevent(app, config, server):
517    """Run WsgiDAV using gevent if gevent (https://www.gevent.org).
518
519    See
520      https://github.com/gevent/gevent/blob/master/src/gevent/pywsgi.py#L1356
521      https://github.com/gevent/gevent/blob/master/src/gevent/server.py#L38
522    for more options.
523    """
524    try:
525        import gevent
526        import gevent.monkey
527        from gevent.pywsgi import WSGIServer
528    except ImportError:
529        _logger.exception("Could not import gevent (http://www.gevent.org).")
530        _logger.error("Try `pip install gevent`.")
531        return False
532
533    gevent.monkey.patch_all()
534
535    info = _get_common_info(config)
536    version = f"gevent/{gevent.__version__}"
537    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
538
539    # Override or add custom args
540    server_args = {
541        "wsgi_app": app,
542        "bind_addr": (config["host"], config["port"]),
543    }
544    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
545    server_args.update(custom_args)
546
547    if info["use_ssl"]:
548        dav_server = WSGIServer(
549            server_args["bind_addr"],
550            app,
551            keyfile=info["ssl_pk"],
552            certfile=info["ssl_cert"],
553            ca_certs=info["ssl_chain"],
554        )
555    else:
556        dav_server = WSGIServer(server_args["bind_addr"], app)
557
558    # If the caller passed a startup event, monkey patch the server to set it
559    # when the request handler loop is entered
560    startup_event = config.get("startup_event")
561    if startup_event:
562
563        def _patched_start():
564            dav_server.start_accepting = org_start  # undo the monkey patch
565            org_start()
566            _logger.info("gevent is ready")
567            startup_event.set()
568
569        org_start = dav_server.start_accepting
570        dav_server.start_accepting = _patched_start
571
572    _logger.info(f"Running {version}")
573    _logger.info(f"Serving on {info['url']} ...")
574    try:
575        gevent.spawn(dav_server.serve_forever())
576    except KeyboardInterrupt:
577        _logger.warning("Caught Ctrl-C, shutting down...")
578    return
579
580
581def _run_gunicorn(app, config, server):
582    """Run WsgiDAV using Gunicorn (https://gunicorn.org)."""
583    try:
584        import gunicorn.app.base
585    except ImportError:
586        _logger.exception("Could not import Gunicorn (https://gunicorn.org).")
587        _logger.error("Try `pip install gunicorn` (UNIX only).")
588        return False
589
590    info = _get_common_info(config)
591
592    class GunicornApplication(gunicorn.app.base.BaseApplication):
593        def __init__(self, app, options=None):
594            self.options = options or {}
595            self.application = app
596            super().__init__()
597
598        def load_config(self):
599            config = {
600                key: value
601                for key, value in self.options.items()
602                if key in self.cfg.settings and value is not None
603            }
604            for key, value in config.items():
605                self.cfg.set(key.lower(), value)
606
607        def load(self):
608            return self.application
609
610    # See https://docs.gunicorn.org/en/latest/settings.html
611    server_args = {
612        "bind": "{}:{}".format(config["host"], config["port"]),
613        "threads": 50,
614        "timeout": 1200,
615    }
616    if info["use_ssl"]:
617        server_args.update(
618            {
619                "keyfile": info["ssl_pk"],
620                "certfile": info["ssl_cert"],
621                "ca_certs": info["ssl_chain"],
622                # "ssl_version": ssl_version
623                # "cert_reqs": ssl_cert_reqs
624                # "ciphers": ssl_ciphers
625            }
626        )
627    # Override or add custom args
628    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
629    server_args.update(custom_args)
630
631    version = f"gunicorn/{gunicorn.__version__}"
632    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
633    _logger.info(f"Running {version} ...")
634
635    GunicornApplication(app, server_args).run()
636
637
638def _run_paste(app, config, server):
639    """Run WsgiDAV using paste.httpserver, if Paste is installed.
640
641    See http://pythonpaste.org/modules/httpserver.html for more options
642    """
643    try:
644        from paste import httpserver
645    except ImportError:
646        _logger.exception(
647            "Could not import paste.httpserver (https://github.com/cdent/paste)."
648        )
649        _logger.error("Try `pip install paste`.")
650        return False
651
652    info = _get_common_info(config)
653
654    version = httpserver.WSGIHandler.server_version
655    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
656
657    # See http://pythonpaste.org/modules/httpserver.html for more options
658    server = httpserver.serve(
659        app,
660        host=config["host"],
661        port=config["port"],
662        server_version=version,
663        # This option enables handling of keep-alive and expect-100:
664        protocol_version="HTTP/1.1",
665        start_loop=False,
666    )
667
668    if config["verbose"] >= 5:
669        __handle_one_request = server.RequestHandlerClass.handle_one_request
670
671        def handle_one_request(self):
672            __handle_one_request(self)
673            if self.close_connection == 1:
674                _logger.debug("HTTP Connection : close")
675            else:
676                _logger.debug("HTTP Connection : continue")
677
678        server.RequestHandlerClass.handle_one_request = handle_one_request
679
680    _logger.info(f"Running {version} ...")
681    host, port = server.server_address
682    if host == "0.0.0.0":
683        _logger.info(f"Serving on 0.0.0.0:{port} view at http://127.0.0.1:{port}")
684    else:
685        _logger.info(f"Serving on {info['url']}")
686
687    try:
688        server.serve_forever()
689    except KeyboardInterrupt:
690        _logger.warning("Caught Ctrl-C, shutting down...")
691    return
692
693
694def _run_uvicorn(app, config, server):
695    """Run WsgiDAV using Uvicorn (https://www.uvicorn.org)."""
696    try:
697        import uvicorn
698    except ImportError:
699        _logger.exception("Could not import Uvicorn (https://www.uvicorn.org).")
700        _logger.error("Try `pip install uvicorn`.")
701        return False
702
703    info = _get_common_info(config)
704
705    # See https://www.uvicorn.org/settings/
706    server_args = {
707        "interface": "wsgi",
708        "host": config["host"],
709        "port": config["port"],
710        # TODO: see _run_cheroot()
711    }
712    if info["use_ssl"]:
713        server_args.update(
714            {
715                "ssl_keyfile": info["ssl_pk"],
716                "ssl_certfile": info["ssl_cert"],
717                "ssl_ca_certs": info["ssl_chain"],
718                # "ssl_keyfile_password": ssl_keyfile_password
719                # "ssl_version": ssl_version
720                # "ssl_cert_reqs": ssl_cert_reqs
721                # "ssl_ciphers": ssl_ciphers
722            }
723        )
724    # Override or add custom args
725    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
726    server_args.update(custom_args)
727
728    version = f"uvicorn/{uvicorn.__version__}"
729    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
730    _logger.info(f"Running {version} ...")
731
732    uvicorn.run(app, **server_args)
733
734
735def _run_wsgiref(app, config, _server):
736    """Run WsgiDAV using wsgiref.simple_server (https://docs.python.org/3/library/wsgiref.html)."""
737    from wsgiref.simple_server import WSGIRequestHandler, make_server
738
739    version = WSGIRequestHandler.server_version
740    version = f"{util.public_wsgidav_info} {version}"  # {util.public_python_info}"
741    _logger.info(f"Running {version} ...")
742
743    _logger.warning(
744        "WARNING: This single threaded server (wsgiref) is not meant for production."
745    )
746    WSGIRequestHandler.server_version = version
747    httpd = make_server(config["host"], config["port"], app)
748    # httpd.RequestHandlerClass.server_version = version
749    try:
750        httpd.serve_forever()
751    except KeyboardInterrupt:
752        _logger.warning("Caught Ctrl-C, shutting down...")
753    return
754
755
756SUPPORTED_SERVERS = {
757    "cheroot": _run_cheroot,
758    "ext-wsgiutils": _run_ext_wsgiutils,
759    "gevent": _run_gevent,
760    "gunicorn": _run_gunicorn,
761    "paste": _run_paste,
762    "uvicorn": _run_uvicorn,
763    "wsgiref": _run_wsgiref,
764}
765
766
767def run():
768    cli_opts, config = _init_config()
769
770    # util.init_logging(config) # now handled in constructor:
771    config["logging"]["enable"] = True
772
773    info = _get_common_info(config)
774
775    app = WsgiDAVApp(config)
776
777    server = config["server"]
778    handler = SUPPORTED_SERVERS.get(server)
779    if not handler:
780        raise RuntimeError(
781            "Unsupported server type {!r} (expected {!r})".format(
782                server, "', '".join(SUPPORTED_SERVERS.keys())
783            )
784        )
785
786    if not use_lxml and config["verbose"] >= 3:
787        _logger.warning(
788            "Could not import lxml: using xml instead (up to 10% slower). "
789            "Consider `pip install lxml`(see https://pypi.python.org/pypi/lxml)."
790        )
791
792    if cli_opts["browse"]:
793        BROWSE_DELAY = 2.0
794
795        def _worker():
796            url = info["url"]
797            url = url.replace("0.0.0.0", "127.0.0.1")
798            _logger.info(f"Starting browser on {url} ...")
799            webbrowser.open(url)
800
801        Timer(BROWSE_DELAY, _worker).start()
802
803    handler(app, config, server)
804    return
805
806
807if __name__ == "__main__":
808    # Just in case...
809    from multiprocessing import freeze_support
810
811    freeze_support()
812
813    run()