1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-06-26 16:45:52 +00:00
Radicale/radicale/__main__.py

214 lines
8.5 KiB
Python
Raw Permalink Normal View History

2021-12-08 21:45:42 +01:00
# This file is part of Radicale - CalDAV and CardDAV server
2017-05-27 17:28:07 +02:00
# Copyright © 2011-2017 Guillaume Ayoub
2024-06-07 08:36:05 +02:00
# Copyright © 2017-2022 Unrud <unrud@outlook.com>
# Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
Radicale executable module.
2018-08-16 08:00:00 +02:00
This module can be executed from a command line with ``$python -m radicale``.
2020-01-12 23:32:28 +01:00
Uses the built-in WSGI server.
"""
import argparse
2019-06-17 04:13:25 +02:00
import contextlib
2016-07-04 14:32:33 +02:00
import os
2018-09-04 03:33:45 +02:00
import signal
import socket
2020-01-21 19:40:02 +01:00
import sys
2021-07-26 20:56:46 +02:00
from types import FrameType
from typing import List, Optional, cast
2018-08-16 08:00:00 +02:00
2021-11-14 23:30:58 +01:00
from radicale import VERSION, config, log, server, storage, types
from radicale.log import logger
2021-07-26 20:56:46 +02:00
def run() -> None:
"""Run Radicale as a standalone server."""
exit_signal_numbers = [signal.SIGTERM, signal.SIGINT]
2021-07-26 20:56:46 +02:00
if sys.platform == "win32":
exit_signal_numbers.append(signal.SIGBREAK)
else:
exit_signal_numbers.append(signal.SIGHUP)
exit_signal_numbers.append(signal.SIGQUIT)
# Raise SystemExit when signal arrives to run cleanup code
# (like destructors, try-finish etc.), otherwise the process exits
# without running any of them
2021-12-19 12:49:26 +01:00
def exit_signal_handler(signal_number: int,
stack_frame: Optional[FrameType]) -> None:
sys.exit(1)
for signal_number in exit_signal_numbers:
signal.signal(signal_number, exit_signal_handler)
log.setup()
# Get command-line arguments
2021-11-14 23:30:58 +01:00
# Configuration options are stored in dest with format "c:SECTION:OPTION"
parser = argparse.ArgumentParser(
2020-10-23 22:26:28 +02:00
prog="radicale", usage="%(prog)s [OPTIONS]", allow_abbrev=False)
parser.add_argument("--version", action="version", version=VERSION)
2017-08-25 19:13:09 +02:00
parser.add_argument("--verify-storage", action="store_true",
help="check the storage for errors and exit")
2021-07-26 20:56:46 +02:00
parser.add_argument("-C", "--config",
help="use specific configuration files", nargs="*")
2021-11-14 23:30:58 +01:00
parser.add_argument("-D", "--debug", action="store_const", const="debug",
dest="c:logging:level", default=argparse.SUPPRESS,
help="print debug information")
2021-11-14 23:30:58 +01:00
for section, section_data in config.DEFAULT_CONFIG_SCHEMA.items():
if section.startswith("_"):
2019-06-17 04:13:25 +02:00
continue
2021-11-14 23:30:58 +01:00
assert ":" not in section # check field separator
assert "-" not in section and "_" not in section # not implemented
group_description = None
if section_data.get("_allow_extra"):
group_description = "additional options allowed"
if section == "headers":
group_description += " (e.g. --headers-Pragma=no-cache)"
elif "type" in section_data:
group_description = "backend specific options omitted"
group = parser.add_argument_group(section, group_description)
2021-11-14 23:30:58 +01:00
for option, data in section_data.items():
2019-06-17 04:13:25 +02:00
if option.startswith("_"):
continue
kwargs = data.copy()
2020-01-19 18:13:05 +01:00
long_name = "--%s-%s" % (section, option.replace("_", "-"))
2021-07-26 20:56:46 +02:00
args: List[str] = list(kwargs.pop("aliases", ()))
args.append(long_name)
2021-11-14 23:30:58 +01:00
kwargs["dest"] = "c:%s:%s" % (section, option)
kwargs["metavar"] = "VALUE"
kwargs["default"] = argparse.SUPPRESS
del kwargs["value"]
with contextlib.suppress(KeyError):
del kwargs["internal"]
if kwargs["type"] == bool:
del kwargs["type"]
2021-11-10 22:16:30 +01:00
opposite_args = list(kwargs.pop("opposite_aliases", ()))
2020-01-19 18:13:05 +01:00
opposite_args.append("--no%s" % long_name[1:])
group.add_argument(*args, nargs="?", const="True", **kwargs)
2021-11-14 23:30:58 +01:00
# Opposite argument
2020-01-19 18:13:05 +01:00
kwargs["help"] = "do not %s (opposite of %s)" % (
kwargs["help"], long_name)
group.add_argument(*opposite_args, action="store_const",
const="False", **kwargs)
else:
2019-06-17 04:13:25 +02:00
del kwargs["type"]
group.add_argument(*args, **kwargs)
args_ns, remaining_args = parser.parse_known_args()
unrecognized_args = []
while remaining_args:
arg = remaining_args.pop(0)
for section, data in config.DEFAULT_CONFIG_SCHEMA.items():
if "type" not in data and not data.get("_allow_extra"):
continue
prefix = "--%s-" % section
if arg.startswith(prefix):
arg = arg[len(prefix):]
break
else:
unrecognized_args.append(arg)
continue
value = ""
if "=" in arg:
arg, value = arg.split("=", maxsplit=1)
elif remaining_args and not remaining_args[0].startswith("-"):
value = remaining_args.pop(0)
option = arg
if not data.get("_allow_extra"): # preserve dash in HTTP header names
option = option.replace("-", "_")
vars(args_ns)["c:%s:%s" % (section, option)] = value
if unrecognized_args:
parser.error("unrecognized arguments: %s" %
" ".join(unrecognized_args))
# Preliminary configure logging
2019-06-17 04:13:25 +02:00
with contextlib.suppress(ValueError):
log.set_level(config.DEFAULT_CONFIG_SCHEMA["logging"]["level"]["type"](
vars(args_ns).get("c:logging:level", "")), True)
2013-01-16 11:16:16 +01:00
# Update Radicale configuration according to arguments
2021-11-14 23:30:58 +01:00
arguments_config: types.MUTABLE_CONFIG = {}
for key, value in vars(args_ns).items():
if key.startswith("c:"):
_, section, option = key.split(":", maxsplit=2)
arguments_config[section] = arguments_config.get(section, {})
arguments_config[section][option] = value
2019-06-17 04:13:25 +02:00
try:
configuration = config.load(config.parse_compound_paths(
config.DEFAULT_CONFIG_PATH,
os.environ.get("RADICALE_CONFIG"),
os.pathsep.join(args_ns.config) if args_ns.config is not None
else None))
2019-06-17 04:13:25 +02:00
if arguments_config:
2021-07-26 20:56:46 +02:00
configuration.update(arguments_config, "command line arguments")
2019-06-17 04:13:25 +02:00
except Exception as e:
2021-07-26 20:56:46 +02:00
logger.critical("Invalid configuration: %s", e, exc_info=True)
2020-01-21 19:40:02 +01:00
sys.exit(1)
2016-10-11 18:17:01 +02:00
# Configure logging
log.set_level(cast(str, configuration.get("logging", "level")), configuration.get("logging", "backtrace_on_debug"))
# Log configuration after logger is configured
2024-06-07 08:35:26 +02:00
default_config_active = True
for source, miss in configuration.sources():
2024-06-07 08:35:26 +02:00
logger.info("%s %s", "Skipped missing/unreadable" if miss else "Loaded", source)
if not miss and source != "default config":
default_config_active = False
if default_config_active:
2024-07-25 15:48:24 +02:00
logger.warning("%s", "No config file found/readable - only default config is active")
2019-06-17 04:13:25 +02:00
2021-07-26 20:56:46 +02:00
if args_ns.verify_storage:
2017-08-25 19:13:09 +02:00
logger.info("Verifying storage")
try:
2020-01-17 12:45:01 +01:00
storage_ = storage.load(configuration)
with storage_.acquire_lock("r"):
if not storage_.verify():
2024-07-24 11:22:49 +02:00
logger.critical("Storage verification failed")
2020-01-21 19:40:02 +01:00
sys.exit(1)
2017-08-25 19:13:09 +02:00
except Exception as e:
2021-07-26 20:56:46 +02:00
logger.critical("An exception occurred during storage "
"verification: %s", e, exc_info=True)
2020-01-21 19:40:02 +01:00
sys.exit(1)
2017-08-25 19:13:09 +02:00
return
# Create a socket pair to notify the server of program shutdown
shutdown_socket, shutdown_socket_out = socket.socketpair()
# Shutdown server when signal arrives
2021-12-19 12:49:26 +01:00
def shutdown_signal_handler(signal_number: int,
stack_frame: Optional[FrameType]) -> None:
shutdown_socket.close()
for signal_number in exit_signal_numbers:
signal.signal(signal_number, shutdown_signal_handler)
try:
server.serve(configuration, shutdown_socket_out)
except Exception as e:
2021-07-26 20:56:46 +02:00
logger.critical("An exception occurred during server startup: %s", e,
2024-03-13 22:19:35 +01:00
exc_info=False)
2020-01-21 19:40:02 +01:00
sys.exit(1)
if __name__ == "__main__":
run()