home · contact · privacy
Server/py: Some refactoring, explaining.
[plomrogue] / plomrogue-server.py
1 import argparse
2 import errno
3 import os
4 import shlex
5 import shutil
6 import time
7
8
9 def setup_server_io():
10     """Fill IO files DB with proper file( path)s. Write process IO test string.
11
12     Ensure IO files directory at server/. Remove any old input file if found.
13     Set up new input file for reading, and new output file for writing. Start
14     output file with process hash line of format PID + " " + floated UNIX time
15     (io_db["teststring"]). Raise SystemExit if file is found at path of either
16     record or save file plus io_db["tmp_suffix"].
17     """
18     def detect_atomic_leftover(path, tmp_suffix):
19         path_tmp = path + tmp_suffix
20         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
21               "aborted previous attempt to write '" + path + "'. Aborting " \
22              "until matter is resolved by removing it from its current path."
23         if os.access(path_tmp, os.F_OK):
24             raise SystemExit(msg)
25     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
26     os.makedirs(io_db["path_server"], exist_ok=True)
27     io_db["file_out"] = open(io_db["path_out"], "w")
28     io_db["file_out"].write(io_db["teststring"] + "\n")
29     io_db["file_out"].flush()
30     if os.access(io_db["path_in"], os.F_OK):
31         os.remove(io_db["path_in"])
32     io_db["file_in"] = open(io_db["path_in"], "w")
33     io_db["file_in"].close()
34     io_db["file_in"] = open(io_db["path_in"], "r")
35     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
36     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
37
38
39 def cleanup_server_io():
40     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
41     def helper(file_key, path_key):
42         if file_key in io_db:
43             io_db[file_key].close()
44             if not io_db["kicked_by_rival"] \
45                and os.access(io_db[path_key], os.F_OK):
46                 os.remove(io_db[path_key])
47     helper("file_out", "path_out")
48     helper("file_in", "path_in")
49     helper("file_worldstate", "path_worldstate")
50     if "file_record" in io_db:
51         io_db["file_record"].close()
52
53
54 def obey(command, prefix, replay=False, do_record=False):
55     """Call function from commands_db mapped to command's first token.
56
57     The command string is tokenized by shlex.split(comments=True). If replay is
58     set, a non-meta command from the commands_db merely triggers obey() on the
59     next command from the records file. Non-meta commands are recorded in
60     non-replay mode if do_record is set. The prefix string is inserted into the
61     server's input message between its beginning 'input ' and ':'. All activity
62     is preceded by a call to server_test().
63     """
64     server_test()
65     print("input " + prefix + ": " + command)
66     try:
67         tokens = shlex.split(command, comments=True)
68     except ValueError as err:
69         print("Can't tokenize command string: " + str(err) + ".")
70         return
71     if len(tokens) > 0 and tokens[0] in commands_db \
72        and len(tokens) >= commands_db[tokens[0]][0] + 1:
73         if commands_db[tokens[0]][1]:
74             commands_db[tokens[0]][2]()
75         elif replay:
76             print("Due to replay mode, reading command as 'go on in record'.")
77             line = io_db["file_record"].readline()
78             if len(line) > 0:
79                 obey(line.rstrip(), io_db["file_record"].prefix
80                      + str(io_db["file_record"].line_n))
81                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
82             else:
83                 print("Reached end of record file.")
84         else:
85             commands_db[tokens[0]][2]()
86             if do_record:
87                 record(command)
88     else:
89         print("Invalid command/argument, or bad number of tokens.")
90
91
92 def record(command):
93     """Append command string plus newline to record file. (Atomic.)"""
94     # This misses some optimizations from the original record(), namely only
95     # finishing the atomic write with expensive flush() and fsync() every 15
96     # seconds unless explicitely forced. Implement as needed.
97     path_tmp = io_db["path_record"] + io_db["tmp_suffix"]
98     if os.access(io_db["path_record"], os.F_OK):
99         shutil.copyfile(io_db["path_record"], path_tmp)
100     file = open(path_tmp, "a")
101     file.write(command + "\n")
102     file.flush()
103     os.fsync(file.fileno())
104     file.close()
105     if os.access(io_db["path_record"], os.F_OK):
106         os.remove(io_db["path_record"])
107     os.rename(path_tmp, io_db["path_record"])
108
109
110 def obey_lines_in_file(path, name, do_record=False):
111     """Call obey() on each line of path's file, use name in input prefix."""
112     file = open(path, "r")
113     line_n = 1
114     for line in file.readlines():
115         obey(line.rstrip(), name + "file line " + str(line_n),
116              do_record=do_record)
117         line_n = line_n + 1
118     file.close()
119
120
121 def parse_command_line_arguments():
122     """Return settings values read from command line arguments."""
123     parser = argparse.ArgumentParser()
124     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
125                         action='store')
126     opts, unknown = parser.parse_known_args()
127     return opts
128
129
130 def server_test():
131     """Ensure valid server out file belonging to current process.
132
133     This is done by comparing io_db["teststring"] to what's found at the start
134     of the current file at io_db["path_out"]. On failure, set
135     io_db["kicked_by_rival"] and raise SystemExit.
136     """
137     if not os.access(io_db["path_out"], os.F_OK):
138         raise SystemExit("Server output file has disappeared.")
139     file = open(io_db["path_out"], "r")
140     test = file.readline().rstrip("\n")
141     file.close()
142     if test != io_db["teststring"]:
143         io_db["kicked_by_rival"] = True
144         msg = "Server test string in server output file does not match. This" \
145               " indicates that the current server process has been " \
146               "superseded by another one."
147         raise SystemExit(msg)
148
149
150 def read_command():
151     """Return next newline-delimited command from server in file.
152
153     Keep building return string until a newline is encountered. Pause between
154     unsuccessful reads, and after too much waiting, run server_test().
155     """
156     wait_on_fail = 1
157     max_wait = 5
158     now = time.time()
159     command = ""
160     while 1:
161         add = io_db["file_in"].readline()
162         if len(add) > 0:
163             command = command + add
164             if len(command) > 0 and "\n" == command[-1]:
165                 command = command[:-1]
166                 break
167         else:
168             time.sleep(wait_on_fail)
169             if now + max_wait < time.time():
170                 server_test()
171                 now = time.time()
172     return command
173
174
175 def command_makeworld():
176     """Mere dummy so far."""
177     print("I would build a whole world now if only I knew how.")
178
179
180 def command_ping():
181     """Send PONG line to server output file."""
182     io_db["file_out"].write("PONG\n")
183     io_db["file_out"].flush()
184
185
186 def command_quit():
187     """Abort server process."""
188     raise SystemExit("received QUIT command")
189
190
191 """Commands database.
192
193 Map command start tokens to ([0]) minimum number of expected command arguments,
194 ([1]) the command's meta-ness (i.e. is it to be written to the record file, is
195 it to be ignored in replay mode if read from server input file), and ([2]) a
196 function to be called on it.
197 """
198 commands_db = {
199     "QUIT": (0, True, command_quit),
200     "PING": (0, True, command_ping),
201     "MAKE_WORLD": (1, False, command_makeworld)
202 }
203
204
205 """World state database,"""
206 world_db = {
207     "turn": 0
208 }
209
210
211 """File IO database."""
212 io_db = {
213     "path_save": "save",
214     "path_record": "record",
215     "path_worldconf": "confserver/world",
216     "path_server": "server/",
217     "path_in": "server/in",
218     "path_out": "server/out",
219     "path_worldstate": "server/worldstate",
220     "tmp_suffix": "_tmp",
221     "kicked_by_rival": False
222 }
223
224
225 try:
226     opts = parse_command_line_arguments()
227     setup_server_io()
228     # print("DUMMY: Run game.")
229     if None != opts.replay:
230         if opts.replay < 1:
231             opts.replay = 1
232         print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
233               " (if so late a turn is to be found).")
234         if not os.access(io_db["path_record"], os.F_OK):
235             raise SystemExit("No record file found to replay.")
236         io_db["file_record"] = open(io_db["path_record"], "r")
237         io_db["file_record"].prefix = "recod file line "
238         io_db["file_record"].line_n = 1
239         while world_db["turn"] < opts.replay:
240             line = io_db["file_record"].readline()
241             if "" == line:
242                 break
243             obey(line.rstrip(), io_db["file_record"].prefix
244                  + str(io_db["file_record"].line_n))
245             io_db["file_record"].line_n = io_db["file_record"].line_n + 1
246         while True:
247             obey(read_command(), "in file", replay=True)
248     else:
249         if os.access(io_db["path_save"], os.F_OK):
250             obey_lines_in_file(io_db["path_save"], "save")
251         else:
252             if not os.access(io_db["path_worldconf"], os.F_OK):
253                 msg = "No world config file from which to start a new world."
254                 raise SystemExit(msg)
255             obey_lines_in_file(io_db["path_worldconf"], "world config ",
256                                do_record=True)
257             obey("MAKE_WORLD " + str(int(time.time())), "in file",
258                  do_record=True)
259         while True:
260             obey(read_command(), "in file", do_record=True)
261 except SystemExit as exit:
262     print("ABORTING: " + exit.args[0])
263 except:
264     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
265     raise
266 finally:
267     cleanup_server_io()
268     # print("DUMMY: (Clean up C heap.)")