|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | # -*- coding: utf-8 -*- |
3 | 3 |
|
| 4 | +# from __future__ import annotations |
| 5 | + |
4 | 6 | import os |
5 | 7 | import sys |
6 | 8 | import yaml |
7 | 9 | import argparse |
8 | 10 | from typing import Any |
| 11 | +from typing import Generator |
9 | 12 | from syncplay import ep_server |
10 | 13 |
|
11 | | -WorkDir = '/data/' |
12 | | -CertDir = '/certs/' |
13 | | -ConfigFile = 'config.yml' |
14 | | - |
15 | | - |
16 | | -def debug(msg: str) -> None: |
17 | | - """ Print out debug information. """ |
18 | | - if 'DEBUG' in os.environ and os.environ['DEBUG'] in ['ON', 'TRUE']: |
19 | | - print(f'\033[90m{msg}\033[0m', file=sys.stderr) |
20 | | - |
21 | | - |
22 | | -def temp_file(file: str, content: str) -> str: |
23 | | - """ Create and save content to temporary files. """ |
24 | | - file = os.path.join('/tmp/', file) |
25 | | - with open(file, 'w') as fp: |
26 | | - fp.write(content) |
27 | | - return file |
28 | | - |
29 | | - |
30 | | -def load_args() -> dict[str, Any]: |
31 | | - """ Loading arguments from the command line. """ |
32 | | - parser = argparse.ArgumentParser(description='Syncplay Docker Bootstrap') |
33 | | - parser.add_argument('-p', '--port', type=int, help='listen port of syncplay server') |
34 | | - parser.add_argument('--password', type=str, help='authentication of syncplay server') |
35 | | - parser.add_argument('--motd', type=str, help='welcome text after the user enters the room') |
36 | | - parser.add_argument('--salt', type=str, help='string used to secure passwords') |
37 | | - parser.add_argument('--random-salt', action='store_true', help='use a randomly generated salt value') |
38 | | - parser.add_argument('--isolate-rooms', action='store_true', help='room isolation enabled') |
39 | | - parser.add_argument('--disable-chat', action='store_true', help='disables the chat feature') |
40 | | - parser.add_argument('--disable-ready', action='store_true', help='disables the readiness indicator feature') |
41 | | - parser.add_argument('--enable-stats', action='store_true', help='enable syncplay server statistics') |
42 | | - parser.add_argument('--enable-tls', action='store_true', help='enable tls support of syncplay server') |
43 | | - parser.add_argument('--persistent', action='store_true', help='enables room persistence') |
44 | | - parser.add_argument('--max-username', type=int, help='maximum length of usernames') |
45 | | - parser.add_argument('--max-chat-message', type=int, help='maximum length of chat messages') |
46 | | - parser.add_argument('--permanent-rooms', type=str, nargs='*', help='permanent rooms of syncplay server') |
47 | | - args = parser.parse_args() |
48 | | - debug(f'Command line arguments -> {args}') |
49 | | - return {k.replace('_', '-'): v for k, v in vars(args).items()} |
50 | | - |
51 | | - |
52 | | -def load_config(args: dict[str, Any], file: str) -> dict[str, Any]: |
53 | | - """ Complete uninitialized arguments from configure file. """ |
54 | | - if not os.path.exists(file): |
| 14 | + |
| 15 | +class SyncplayBoot: |
| 16 | + """ Handle Syncplay bootstrap arguments. """ |
| 17 | + def __debug(self, msg: str) -> None: |
| 18 | + """ Print out debug information. """ |
| 19 | + if self.__debug_mode: |
| 20 | + print(f'\033[90m{msg}\033[0m', file=sys.stderr) |
| 21 | + |
| 22 | + def __build_parser(self) -> Generator: |
| 23 | + """ Build arguments parser for Syncplay bootstrap. """ |
| 24 | + parser = argparse.ArgumentParser(description='Syncplay Docker Bootstrap') |
| 25 | + yield parser.add_argument('-p', '--port', type=int, help='listen port of syncplay server') |
| 26 | + yield parser.add_argument('--password', type=str, help='authentication of syncplay server') |
| 27 | + yield parser.add_argument('--motd', type=str, help='welcome text after the user enters the room') |
| 28 | + yield parser.add_argument('--salt', type=str, help='string used to secure passwords') |
| 29 | + yield parser.add_argument('--random-salt', action='store_true', help='use a randomly generated salt value') |
| 30 | + yield parser.add_argument('--isolate-rooms', action='store_true', help='room isolation enabled') |
| 31 | + yield parser.add_argument('--disable-chat', action='store_true', help='disables the chat feature') |
| 32 | + yield parser.add_argument('--disable-ready', action='store_true', help='disables the readiness indicator feature') |
| 33 | + yield parser.add_argument('--enable-stats', action='store_true', help='enable syncplay server statistics') |
| 34 | + yield parser.add_argument('--enable-tls', action='store_true', help='enable tls support of syncplay server') |
| 35 | + yield parser.add_argument('--persistent', action='store_true', help='enables room persistence') |
| 36 | + yield parser.add_argument('--max-username', type=int, help='maximum length of usernames') |
| 37 | + yield parser.add_argument('--max-chat-message', type=int, help='maximum length of chat messages') |
| 38 | + yield parser.add_argument('--permanent-rooms', type=str, nargs='*', help='permanent rooms of syncplay server') |
| 39 | + self.__parser = parser |
| 40 | + |
| 41 | + def __build_options(self) -> Generator: |
| 42 | + """ Build options list for Syncplay bootstrap. """ |
| 43 | + for action in [x for x in self.__build_parser()]: |
| 44 | + is_list = type(action.nargs) is str |
| 45 | + opt_type = bool if action.type is None else action.type |
| 46 | + yield action.dest, opt_type, is_list |
| 47 | + |
| 48 | + def __init__(self, args: list[str], config: dict[str, Any], |
| 49 | + work_dir: str, cert_dir: str, debug_mode: bool = False): |
| 50 | + self.__work_dir = work_dir |
| 51 | + self.__cert_dir = cert_dir |
| 52 | + self.__debug_mode = debug_mode |
| 53 | + self.__options = [x for x in self.__build_options()] # list[(NAME, TYPE, IS_LIST)] |
| 54 | + self.__debug(f'Bootstrap options -> {self.__options}\n') |
| 55 | + |
| 56 | + env_opts = self.__load_from_env() |
| 57 | + self.__debug(f'Environment options -> {env_opts}\n') |
| 58 | + cfg_opts = self.__load_from_config(config) |
| 59 | + self.__debug(f'Configure file options -> {cfg_opts}\n') |
| 60 | + cli_opts = self.__load_from_args(args) |
| 61 | + self.__debug(f'Command line options -> {cli_opts}\n') |
| 62 | + |
| 63 | + self.__opts = env_opts | cfg_opts | cli_opts |
| 64 | + self.__debug(f'Bootstrap final options -> {self.__opts}') |
| 65 | + |
| 66 | + def __load_from_args(self, raw_args: list[str]) -> dict[str, Any]: |
| 67 | + """ Loading options from command line arguments. """ |
| 68 | + args = self.__parser.parse_args(raw_args) |
| 69 | + self.__debug(f'Command line arguments -> {args}') |
| 70 | + arg_filter = lambda x: x is not None and x is not False |
| 71 | + return {x: y for x, y in vars(args).items() if arg_filter(y)} |
| 72 | + |
| 73 | + def __load_from_config(self, config: dict[str, Any]) -> dict[str, Any]: |
| 74 | + """ Loading options from configure file. """ |
| 75 | + self.__debug(f'Configure file -> {config}') |
| 76 | + options = {x[0].replace('_', '-'): x[0] for x in self.__options} |
| 77 | + return {options[x]: config[x] for x in options if x in config} |
| 78 | + |
| 79 | + def __load_from_env(self) -> dict[str, Any]: |
| 80 | + """ Loading options from environment variables. """ |
| 81 | + def __convert(opt_raw: str, opt_field: str, opt_type: type) -> tuple[str, Any]: |
| 82 | + if opt_type is str: |
| 83 | + return opt_field, opt_raw |
| 84 | + elif opt_type is int: |
| 85 | + return opt_field, int(opt_raw) |
| 86 | + elif opt_type is bool: |
| 87 | + return opt_field, opt_raw.upper() in ['ON', 'TRUE'] |
| 88 | + |
| 89 | + self.__debug(f'Environment variables -> {os.environ}') |
| 90 | + options = {x.upper(): (x, t) for x, t, is_list in self.__options if not is_list} # filter non-list options |
| 91 | + return dict([__convert(os.environ[x], *y) for x, y in options.items() if x in os.environ]) |
| 92 | + |
| 93 | + @staticmethod |
| 94 | + def __temp_file(file: str, content: str) -> str: |
| 95 | + """ Create and save content to temporary files. """ |
| 96 | + file = os.path.join('/tmp/', file) |
| 97 | + with open(file, 'w') as fp: |
| 98 | + fp.write(content) |
| 99 | + return file |
| 100 | + |
| 101 | + def release(self) -> list[str]: |
| 102 | + """ Construct the startup arguments for syncplay server. """ |
| 103 | + args = ['--port', str(self.__opts.get('port', 8999))] |
| 104 | + if 'password' in self.__opts: |
| 105 | + args += ['--password', self.__opts['password']] |
| 106 | + if 'motd' in self.__opts: |
| 107 | + args += ['--motd-file', SyncplayBoot.__temp_file('motd.data', self.__opts['motd'])] |
| 108 | + |
| 109 | + salt = self.__opts.get('salt', None if 'random_salt' in self.__opts else '') |
| 110 | + if salt is not None: |
| 111 | + args += ['--salt', salt] # using random salt without this option |
| 112 | + for opt in ['isolate_rooms', 'disable_chat', 'disable_ready']: |
| 113 | + if opt in self.__opts: |
| 114 | + args.append(f'--{opt.replace("_", "-")}') |
| 115 | + |
| 116 | + if 'enable_stats' in self.__opts: |
| 117 | + args += ['--stats-db-file', os.path.join(self.__work_dir, 'stats.db')] |
| 118 | + if 'enable_tls' in self.__opts: |
| 119 | + args += ['--tls', self.__cert_dir] |
| 120 | + if 'persistent' in self.__opts: |
| 121 | + args += ['--rooms-db-file', os.path.join(self.__work_dir, 'rooms.db')] |
| 122 | + |
| 123 | + if 'max_username' in self.__opts: |
| 124 | + args += ['--max-username-length', str(self.__opts['max_username'])] |
| 125 | + if 'max_chat_message' in self.__opts: |
| 126 | + args += ['--max-chat-message-length', str(self.__opts['max_chat_message'])] |
| 127 | + if 'permanent_rooms' in self.__opts: |
| 128 | + rooms = '\n'.join(self.__opts['permanent_rooms']) |
| 129 | + args += ['--permanent-rooms-file', SyncplayBoot.__temp_file('rooms.list', rooms)] |
| 130 | + |
| 131 | + self.__debug(f'Syncplay startup arguments -> {args}') |
55 | 132 | return args |
56 | | - config = yaml.safe_load(open(file).read()) |
57 | | - options = [ |
58 | | - 'port', 'password', 'motd', 'salt', 'random-salt', |
59 | | - 'isolate-rooms', 'disable-chat', 'disable-ready', |
60 | | - 'enable-stats', 'enable-tls', 'persistent', |
61 | | - 'max-username', 'max-chat-message', 'permanent-rooms', |
62 | | - ] |
63 | | - override = {x: config[x] for x in options if not args[x] and x in config} |
64 | | - debug(f'Configure file override -> {override}') |
65 | | - return args | override |
66 | | - |
67 | | - |
68 | | -def build_args(opts: dict): |
69 | | - """ Construct the startup arguments for syncplay server. """ |
70 | | - args = ['--port', str(opts.get('port', 8999))] |
71 | | - if 'password' in opts: |
72 | | - args += ['--password', opts['password']] |
73 | | - if 'motd' in opts: |
74 | | - args += ['--motd-file', temp_file('motd.data', opts['motd'])] |
75 | | - |
76 | | - salt = opts.get('salt', None if 'random-salt' in opts else '') |
77 | | - if salt is not None: |
78 | | - args += ['--salt', salt] # using random salt without this option |
79 | | - for opt in ['isolate-rooms', 'disable-chat', 'disable-ready']: |
80 | | - if opt in opts: |
81 | | - args.append(f'--{opt}') |
82 | | - |
83 | | - if 'enable-stats' in opts: |
84 | | - args += ['--stats-db-file', os.path.join(WorkDir, 'stats.db')] |
85 | | - if 'enable-tls' in opts: |
86 | | - args += ['--tls', CertDir] |
87 | | - if 'persistent' in opts: |
88 | | - args += ['--rooms-db-file', os.path.join(WorkDir, 'rooms.db')] |
89 | | - |
90 | | - if 'max-username' in opts: |
91 | | - args += ['--max-username-length', str(opts['max-username'])] |
92 | | - if 'max-chat-message' in opts: |
93 | | - args += ['--max-chat-message-length', str(opts['max-chat-message'])] |
94 | | - if 'permanent-rooms' in opts: |
95 | | - rooms = '\n'.join(opts['permanent-rooms']) |
96 | | - args += ['--permanent-rooms-file', temp_file('rooms.list', rooms)] |
97 | | - return args |
| 133 | + |
| 134 | + |
| 135 | +def syncplay_boot() -> None: |
| 136 | + """ Bootstrap the syncplay server. """ |
| 137 | + work_dir = os.environ.get('WORK_DIR', '/data') |
| 138 | + cert_dir = os.environ.get('CERT_DIR', '/certs') |
| 139 | + config_file = os.environ.get('CONFIG', 'config.yml') |
| 140 | + debug_mode = os.environ.get('DEBUG', '').upper() in ['ON', 'TRUE'] |
| 141 | + |
| 142 | + config = yaml.safe_load(open(config_file).read()) if os.path.exists(config_file) else {} |
| 143 | + bootstrap = SyncplayBoot(sys.argv[1:], config, work_dir, cert_dir, debug_mode) |
| 144 | + sys.argv = ['syncplay'] + bootstrap.release() |
98 | 145 |
|
99 | 146 |
|
100 | 147 | if __name__ == '__main__': |
101 | | - origin_args = load_config(load_args(), os.path.join(WorkDir, ConfigFile)) |
102 | | - origin_args = {k: v for k, v in origin_args.items() if v is not None and v is not False} # remove invalid items |
103 | | - debug(f'Parsed arguments -> {origin_args}') |
104 | | - syncplay_args = build_args(origin_args) |
105 | | - debug(f'Syncplay startup arguments -> {syncplay_args}') |
106 | | - sys.argv = ['syncplay'] + syncplay_args |
| 148 | + syncplay_boot() |
107 | 149 | sys.exit(ep_server.main()) |
0 commit comments