|
1 | 1 | #!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
2 | 3 |
|
3 | 4 | import os |
4 | 5 | import sys |
| 6 | +import yaml |
| 7 | +import argparse |
| 8 | +from typing import Any |
5 | 9 | from syncplay import ep_server |
6 | 10 |
|
7 | | - |
8 | | -def checkOpt(args: list, option: str) -> bool: |
9 | | - if option not in args: # option not found |
10 | | - return False |
11 | | - args.remove(option) # remove target option |
12 | | - return True |
13 | | - |
14 | | - |
15 | | -def fetchOpt(args: list, option: str, default): |
16 | | - if option not in args: # option not found |
17 | | - return default |
18 | | - index = args.index(option) |
19 | | - if index + 1 == len(args): |
20 | | - print('Error: `%s` missing value' % option, file = sys.stderr) |
21 | | - sys.exit(1) |
22 | | - targetVal = args[index + 1] |
23 | | - del sys.argv[index : index + 2] # remove target option and value |
24 | | - return targetVal |
25 | | - |
26 | | - |
27 | | -isDebug = checkOpt(sys.argv, '--debug') |
28 | | - |
29 | | - |
30 | | -portValue = None # no specify in default |
31 | | -if 'PORT' in os.environ: # `PORT` env variable |
32 | | - portValue = os.environ['PORT'] |
33 | | -portValue = fetchOpt(sys.argv, '--port', portValue) |
34 | | - |
35 | | - |
36 | | -passwdStr = None # no password in default |
37 | | -if 'PASSWD' in os.environ: # `PASSWD` env variable |
38 | | - passwdStr = os.environ['PASSWD'] |
39 | | -passwdStr = fetchOpt(sys.argv, '--password', passwdStr) |
40 | | - |
41 | | - |
42 | | -saltValue = '' # using empty string in default |
43 | | -if 'SALT' in os.environ: # `SALT` env variable |
44 | | - saltValue = os.environ['SALT'] |
45 | | -if checkOpt(sys.argv, '--random-salt'): |
46 | | - saltValue = None |
47 | | -saltValue = fetchOpt(sys.argv, '--salt', saltValue) |
48 | | - |
49 | | - |
50 | | -isolateRoom = False # disable isolate room in default |
51 | | -if 'ISOLATE' in os.environ and os.environ['ISOLATE'] in ['ON', 'TRUE']: |
52 | | - isolateRoom = True |
53 | | -if checkOpt(sys.argv, '--isolate-room'): |
54 | | - isolateRoom = True |
55 | | - |
56 | | - |
57 | | -tlsPath = '/certs' |
58 | | -if 'TLS_PATH' in os.environ: # `TLS_PATH` env variable |
59 | | - tlsPath = os.environ['TLS_PATH'] |
60 | | -tlsPath = fetchOpt(sys.argv, '--tls', tlsPath) |
61 | | - |
62 | | -enableTls = False |
63 | | -if checkOpt(sys.argv, '--enable-tls'): |
64 | | - enableTls = True |
65 | | -if 'TLS' in os.environ and os.environ['TLS'] in ['ON', 'TRUE']: |
66 | | - enableTls = True |
67 | | - |
68 | | - |
69 | | -motdMessage = None # without motd message in default |
70 | | -if 'MOTD' in os.environ: # `MOTD` env variable |
71 | | - motdMessage = os.environ['MOTD'] |
72 | | -motdMessage = fetchOpt(sys.argv, '--motd', motdMessage) |
73 | | - |
74 | | -motdFile = fetchOpt(sys.argv, '--motd-file', None) |
75 | | -if motdFile is not None: |
76 | | - motdMessage = None # cover motd message |
77 | | -elif motdMessage is not None: |
78 | | - motdFile = '/app/syncplay/motd' |
79 | | - os.system('mkdir -p /app/syncplay/') |
80 | | - with open(motdFile, mode = 'w', encoding = 'utf-8') as fileObj: |
81 | | - fileObj.write(motdMessage) |
82 | | - |
83 | | - |
84 | | -if isDebug: # print debug log |
85 | | - if portValue is not None: |
86 | | - print('Port -> %s' % portValue, file = sys.stderr) |
87 | | - |
88 | | - if saltValue is None: |
89 | | - print('Using random salt', file = sys.stderr) |
90 | | - else: |
91 | | - print('Salt -> `%s`' % saltValue, file = sys.stderr) |
92 | | - |
93 | | - if isolateRoom: |
94 | | - print('Isolate room enabled', file = sys.stderr) |
95 | | - |
96 | | - if passwdStr is None: |
97 | | - print('Running without password', file = sys.stderr) |
98 | | - else: |
99 | | - print('Password -> `%s`' % passwdStr, file = sys.stderr) |
100 | | - |
101 | | - if enableTls: |
102 | | - print('TLS enabled -> `%s`' % tlsPath, file = sys.stderr) |
103 | | - |
104 | | - if motdFile is not None: |
105 | | - print('MOTD File -> `%s`' % motdFile, file = sys.stderr) |
106 | | - if motdMessage is not None: |
107 | | - print('MOTD message -> `%s`' % motdMessage, file = sys.stderr) |
108 | | - |
109 | | - |
110 | | -if portValue is not None: |
111 | | - sys.argv += ['--port', portValue] |
112 | | -if passwdStr is not None: |
113 | | - sys.argv += ['--password', passwdStr] |
114 | | -if saltValue is not None: |
115 | | - sys.argv += ['--salt', saltValue] |
116 | | -if enableTls: |
117 | | - sys.argv += ['--tls', tlsPath] |
118 | | -if isolateRoom: |
119 | | - sys.argv += ['--isolate-room'] |
120 | | -if motdFile is not None: |
121 | | - sys.argv += ['--motd-file', motdFile] |
122 | | - |
123 | | - |
124 | | -if isDebug: # print debug log |
125 | | - print('Boot args -> %s' % sys.argv, file = sys.stderr) |
126 | | - |
127 | | - |
128 | | -sys.exit(ep_server.main()) |
| 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(f'{content}\n') |
| 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): |
| 55 | + 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', 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 |
| 98 | + |
| 99 | + |
| 100 | +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 |
| 107 | + sys.exit(ep_server.main()) |
0 commit comments