mopidy / mopidy
1 0
import logging
2 0
import signal
3 0
import sys
4

5 0
import pykka.debug
6

7 0
from mopidy import commands
8 0
from mopidy import config as config_lib
9 0
from mopidy import ext
10 0
from mopidy.internal import log, path, process, versioning
11 0
from mopidy.internal.gi import Gst  # noqa: F401
12

13 0
try:
14
    # Make GLib's mainloop the event loop for python-dbus
15 0
    import dbus.mainloop.glib
16

17 0
    dbus.mainloop.glib.threads_init()
18 0
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
19 0
except ImportError:
20 0
    pass
21

22

23 0
logger = logging.getLogger(__name__)
24

25

26 0
def main():
27 0
    log.bootstrap_delayed_logging()
28 0
    logger.info(f"Starting Mopidy {versioning.get_version()}")
29

30 0
    signal.signal(signal.SIGTERM, process.sigterm_handler)
31
    # Windows does not have signal.SIGUSR1
32 0
    if hasattr(signal, "SIGUSR1"):
33 0
        signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks)
34

35 0
    try:
36 0
        registry = ext.Registry()
37

38 0
        root_cmd = commands.RootCommand()
39 0
        config_cmd = commands.ConfigCommand()
40 0
        deps_cmd = commands.DepsCommand()
41

42 0
        root_cmd.set(extension=None, registry=registry)
43 0
        root_cmd.add_child("config", config_cmd)
44 0
        root_cmd.add_child("deps", deps_cmd)
45

46 0
        extensions_data = ext.load_extensions()
47

48 0
        for data in extensions_data:
49 0
            if data.command:  # TODO: check isinstance?
50 0
                data.command.set(extension=data.extension)
51 0
                root_cmd.add_child(data.extension.ext_name, data.command)
52

53 0
        args = root_cmd.parse(sys.argv[1:])
54

55 0
        config, config_errors = config_lib.load(
56
            args.config_files,
57
            [d.config_schema for d in extensions_data],
58
            [d.config_defaults for d in extensions_data],
59
            args.config_overrides,
60
        )
61

62 0
        create_core_dirs(config)
63 0
        create_initial_config_file(args, extensions_data)
64

65 0
        log.setup_logging(
66
            config, args.base_verbosity_level, args.verbosity_level
67
        )
68

69 0
        extensions = {
70
            "validate": [],
71
            "config": [],
72
            "disabled": [],
73
            "enabled": [],
74
        }
75 0
        for data in extensions_data:
76 0
            extension = data.extension
77

78
            # TODO: factor out all of this to a helper that can be tested
79 0
            if not ext.validate_extension_data(data):
80 0
                config[extension.ext_name] = {"enabled": False}
81 0
                config_errors[extension.ext_name] = {
82
                    "enabled": "extension disabled by self check."
83
                }
84 0
                extensions["validate"].append(extension)
85 0
            elif not config[extension.ext_name]["enabled"]:
86 0
                config[extension.ext_name] = {"enabled": False}
87 0
                config_errors[extension.ext_name] = {
88
                    "enabled": "extension disabled by user config."
89
                }
90 0
                extensions["disabled"].append(extension)
91 0
            elif config_errors.get(extension.ext_name):
92 0
                config[extension.ext_name]["enabled"] = False
93 0
                config_errors[extension.ext_name][
94
                    "enabled"
95
                ] = "extension disabled due to config errors."
96 0
                extensions["config"].append(extension)
97
            else:
98 0
                extensions["enabled"].append(extension)
99

100 0
        log_extension_info(
101
            [d.extension for d in extensions_data], extensions["enabled"]
102
        )
103

104
        # Config and deps commands are simply special cased for now.
105 0
        if args.command == config_cmd:
106 0
            schemas = [d.config_schema for d in extensions_data]
107 0
            return args.command.run(config, config_errors, schemas)
108 0
        elif args.command == deps_cmd:
109 0
            return args.command.run()
110

111 0
        check_config_errors(config, config_errors, extensions)
112

113 0
        if not extensions["enabled"]:
114 0
            logger.error("No extension enabled, exiting...")
115 0
            sys.exit(1)
116

117
        # Read-only config from here on, please.
118 0
        proxied_config = config_lib.Proxy(config)
119

120 0
        if args.extension and args.extension not in extensions["enabled"]:
121 0
            logger.error(
122
                "Unable to run command provided by disabled extension %s",
123
                args.extension.ext_name,
124
            )
125 0
            return 1
126

127 0
        for extension in extensions["enabled"]:
128 0
            try:
129 0
                extension.setup(registry)
130 0
            except Exception:
131
                # TODO: would be nice a transactional registry. But sadly this
132
                # is a bit tricky since our current API is giving out a mutable
133
                # list. We might however be able to replace this with a
134
                # collections.Sequence to provide a RO view.
135 0
                logger.exception(
136
                    f"Extension {extension.ext_name} failed during setup. "
137
                    f"This might have left the registry in a bad state."
138
                )
139

140
        # Anything that wants to exit after this point must use
141
        # mopidy.internal.process.exit_process as actors can have been started.
142 0
        try:
143 0
            return args.command.run(args, proxied_config)
144 0
        except NotImplementedError:
145 0
            print(root_cmd.format_help())
146 0
            return 1
147

148 0
    except KeyboardInterrupt:
149 0
        pass
150 0
    except Exception as ex:
151 0
        logger.exception(ex)
152 0
        raise
153

154

155 0
def create_core_dirs(config):
156 0
    path.get_or_create_dir(config["core"]["cache_dir"])
157 0
    path.get_or_create_dir(config["core"]["config_dir"])
158 0
    path.get_or_create_dir(config["core"]["data_dir"])
159

160

161 0
def create_initial_config_file(args, extensions_data):
162
    """Initialize whatever the last config file is with defaults"""
163

164 0
    config_file = path.expand_path(args.config_files[-1])
165

166 0
    if config_file.exists():
167 0
        return
168

169 0
    try:
170 0
        default = config_lib.format_initial(extensions_data)
171 0
        path.get_or_create_file(
172
            config_file,
173
            mkdir=False,
174
            content=default.encode(errors="surrogateescape"),
175
        )
176 0
        logger.info(f"Initialized {config_file.as_uri()} with default config")
177 0
    except OSError as exc:
178 0
        logger.warning(
179
            f"Unable to initialize {config_file.as_uri()} with default config: {exc}"
180
        )
181

182

183 0
def log_extension_info(all_extensions, enabled_extensions):
184
    # TODO: distinguish disabled vs blocked by env?
185 0
    enabled_names = {e.ext_name for e in enabled_extensions}
186 0
    disabled_names = {e.ext_name for e in all_extensions} - enabled_names
187 0
    logger.info("Enabled extensions: %s", ", ".join(enabled_names) or "none")
188 0
    logger.info("Disabled extensions: %s", ", ".join(disabled_names) or "none")
189

190

191 0
def check_config_errors(config, errors, extensions):
192 0
    fatal_errors = []
193 0
    extension_names = {}
194 0
    all_extension_names = set()
195

196 0
    for state in extensions:
197 0
        extension_names[state] = {e.ext_name for e in extensions[state]}
198 0
        all_extension_names.update(extension_names[state])
199

200 0
    for section in sorted(errors):
201 0
        if not errors[section]:
202 0
            continue
203

204 0
        if section not in all_extension_names:
205 0
            logger.warning(f"Found fatal {section} configuration errors:")
206 0
            fatal_errors.append(section)
207 0
        elif section in extension_names["config"]:
208 0
            del errors[section]["enabled"]
209 0
            logger.warning(
210
                f"Found {section} configuration errors. "
211
                f"The extension has been automatically disabled:"
212
            )
213
        else:
214 0
            continue
215

216 0
        for field, msg in errors[section].items():
217 0
            logger.warning(f"  {section}/{field} {msg}")
218

219 0
    if extensions["config"]:
220 0
        logger.warning(
221
            "Please fix the extension configuration errors or "
222
            "disable the extensions to silence these messages."
223
        )
224

225 0
    if fatal_errors:
226 0
        logger.error("Please fix fatal configuration errors, exiting...")
227 0
        sys.exit(1)
228

229

230 0
if __name__ == "__main__":
231 0
    sys.exit(main())

Read our documentation on viewing source code .

Loading