Skip to content

Commit 5a5e1de

Browse files
committed
refactor: syncplay bootstrap process
1 parent 6bdcb83 commit 5a5e1de

File tree

1 file changed

+134
-92
lines changed

1 file changed

+134
-92
lines changed

boot.py

Lines changed: 134 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,149 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
33

4+
# from __future__ import annotations
5+
46
import os
57
import sys
68
import yaml
79
import argparse
810
from typing import Any
11+
from typing import Generator
912
from syncplay import ep_server
1013

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}')
55132
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()
98145

99146

100147
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()
107149
sys.exit(ep_server.main())

0 commit comments

Comments
 (0)