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>
|
2011-11-03 17:47:35 +01:00
|
|
|
#
|
|
|
|
# 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.
|
2011-11-03 17:47:35 +01:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
2016-10-12 14:30:18 +02:00
|
|
|
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
|
2020-10-04 14:11:43 +02:00
|
|
|
import socket
|
2020-01-21 19:40:02 +01:00
|
|
|
import sys
|
2021-07-26 20:56:46 +02:00
|
|
|
from types import FrameType
|
2021-12-19 12:58:35 +01:00
|
|
|
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
|
2018-08-16 07:59:55 +02:00
|
|
|
from radicale.log import logger
|
2011-11-03 17:47:35 +01:00
|
|
|
|
|
|
|
|
2021-07-26 20:56:46 +02:00
|
|
|
def run() -> None:
|
2011-11-03 17:47:35 +01:00
|
|
|
"""Run Radicale as a standalone server."""
|
2020-10-04 14:11:43 +02:00
|
|
|
exit_signal_numbers = [signal.SIGTERM, signal.SIGINT]
|
2021-07-26 20:56:46 +02:00
|
|
|
if sys.platform == "win32":
|
2020-10-04 14:11:43 +02:00
|
|
|
exit_signal_numbers.append(signal.SIGBREAK)
|
2022-02-01 17:53:46 +01:00
|
|
|
else:
|
|
|
|
exit_signal_numbers.append(signal.SIGHUP)
|
|
|
|
exit_signal_numbers.append(signal.SIGQUIT)
|
2020-08-31 13:54:47 +02:00
|
|
|
|
|
|
|
# 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,
|
2021-12-19 12:58:35 +01:00
|
|
|
stack_frame: Optional[FrameType]) -> None:
|
2020-08-31 13:54:47 +02:00
|
|
|
sys.exit(1)
|
2020-10-04 14:11:43 +02:00
|
|
|
for signal_number in exit_signal_numbers:
|
|
|
|
signal.signal(signal_number, exit_signal_handler)
|
2020-08-31 13:54:47 +02:00
|
|
|
|
2018-08-16 07:59:55 +02:00
|
|
|
log.setup()
|
|
|
|
|
2016-10-12 14:30:18 +02:00
|
|
|
# Get command-line arguments
|
2021-11-14 23:30:58 +01:00
|
|
|
# Configuration options are stored in dest with format "c:SECTION:OPTION"
|
2020-08-31 13:54:47 +02:00
|
|
|
parser = argparse.ArgumentParser(
|
2020-10-23 22:26:28 +02:00
|
|
|
prog="radicale", usage="%(prog)s [OPTIONS]", allow_abbrev=False)
|
2016-10-12 14:30:18 +02:00
|
|
|
|
|
|
|
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",
|
2021-12-24 18:00:09 +01:00
|
|
|
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,
|
2018-08-16 08:00:02 +02:00
|
|
|
help="print debug information")
|
2016-10-12 14:30:18 +02:00
|
|
|
|
2021-11-14 23:30:58 +01:00
|
|
|
for section, section_data in config.DEFAULT_CONFIG_SCHEMA.items():
|
2020-02-19 09:50:27 +01:00
|
|
|
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
|
2021-11-14 23:30:59 +01:00
|
|
|
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
|
2016-10-12 14:30:18 +02:00
|
|
|
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", ()))
|
2016-10-12 14:30:18 +02:00
|
|
|
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
|
2017-05-31 11:08:32 +02:00
|
|
|
del kwargs["value"]
|
2020-02-19 09:50:27 +01:00
|
|
|
with contextlib.suppress(KeyError):
|
2017-06-21 09:48:57 +02:00
|
|
|
del kwargs["internal"]
|
2016-10-12 14:30:18 +02:00
|
|
|
|
2017-05-31 11:08:32 +02:00
|
|
|
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:])
|
2021-11-14 23:30:59 +01:00
|
|
|
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)" % (
|
2016-10-12 14:30:18 +02:00
|
|
|
kwargs["help"], long_name)
|
2021-11-14 23:30:59 +01:00
|
|
|
group.add_argument(*opposite_args, action="store_const",
|
|
|
|
const="False", **kwargs)
|
2016-10-12 14:30:18 +02:00
|
|
|
else:
|
2019-06-17 04:13:25 +02:00
|
|
|
del kwargs["type"]
|
2016-10-12 14:30:18 +02:00
|
|
|
group.add_argument(*args, **kwargs)
|
|
|
|
|
2021-11-14 23:30:59 +01:00
|
|
|
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))
|
2018-08-16 07:59:55 +02:00
|
|
|
|
|
|
|
# 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"](
|
2024-06-09 13:42:08 +02:00
|
|
|
vars(args_ns).get("c:logging:level", "")), True)
|
2013-01-16 11:16:16 +01:00
|
|
|
|
2016-10-12 14:30:18 +02: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"),
|
2021-12-24 18:13:18 +01:00
|
|
|
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
|
|
|
|
2018-08-16 07:59:55 +02:00
|
|
|
# Configure logging
|
2024-06-09 13:42:08 +02:00
|
|
|
log.set_level(cast(str, configuration.get("logging", "level")), configuration.get("logging", "backtrace_on_debug"))
|
2013-06-04 15:12:06 +02:00
|
|
|
|
2020-01-12 23:32:25 +01:00
|
|
|
# Log configuration after logger is configured
|
2024-06-07 08:35:26 +02:00
|
|
|
default_config_active = True
|
2020-02-19 09:48:42 +01:00
|
|
|
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
|
|
|
|
|
2020-10-04 14:11:43 +02:00
|
|
|
# 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,
|
2021-12-19 12:58:35 +01:00
|
|
|
stack_frame: Optional[FrameType]) -> None:
|
2020-10-04 14:11:43 +02:00
|
|
|
shutdown_socket.close()
|
|
|
|
for signal_number in exit_signal_numbers:
|
|
|
|
signal.signal(signal_number, shutdown_signal_handler)
|
|
|
|
|
2016-08-25 04:33:14 +02:00
|
|
|
try:
|
2020-10-04 14:11:43 +02:00
|
|
|
server.serve(configuration, shutdown_socket_out)
|
2017-05-31 11:08:32 +02:00
|
|
|
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)
|
2016-07-05 17:50:40 +02:00
|
|
|
|
|
|
|
|
2012-03-01 10:40:15 +01:00
|
|
|
if __name__ == "__main__":
|
2011-11-03 17:47:35 +01:00
|
|
|
run()
|