home · contact · privacy
Server/py: Make proper command_seedmap() refer to (dummy) remake_map().
[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. If not, and do do_record is set,
60     non-meta commands are recorded via record(), and save_world() is called.
61     The prefix string is inserted into the server's input message between its
62     beginning 'input ' & ':'. All activity is preceded by a server_test() call.
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](*tokens[1:])
86             if do_record:
87                 record(command)
88                 save_world()
89     else:
90         print("Invalid command/argument, or bad number of tokens.")
91
92
93 def atomic_write(path, text, do_append=False):
94     """Atomic write of text to file at path, appended if do_append is set."""
95     path_tmp = path + io_db["tmp_suffix"]
96     mode = "w"
97     if do_append:
98         mode = "a"
99         if os.access(path, os.F_OK):
100             shutil.copyfile(path, path_tmp)
101     file = open(path_tmp, mode)
102     file.write(text)
103     file.flush()
104     os.fsync(file.fileno())
105     file.close()
106     if os.access(path, os.F_OK):
107         os.remove(path)
108     os.rename(path_tmp, path)
109
110
111 def record(command):
112     """Append command string plus newline to record file. (Atomic.)"""
113     # This misses some optimizations from the original record(), namely only
114     # finishing the atomic write with expensive flush() and fsync() every 15
115     # seconds unless explicitely forced. Implement as needed.
116     atomic_write(io_db["path_record"], command + "\n", do_append=True)
117
118
119 def save_world():
120     # Dummy for saving all commands to reconstruct current world state.
121     # Misses same optimizations as record() from the original record().
122     atomic_write(io_db["path_save"],
123                  "PLAYER_TYPE " + str(world_db["PLAYER_TYPE"]) + "\n" +
124                  "TURN " + str(world_db["TURN"]) + "\n" +
125                  "SEED_RANDOMNESS " + str(world_db["SEED_RANDOMNESS"]) + "\n" +
126                  "SEED_MAP " + str(world_db["SEED_MAP"]) + "\n")
127
128
129 def obey_lines_in_file(path, name, do_record=False):
130     """Call obey() on each line of path's file, use name in input prefix."""
131     file = open(path, "r")
132     line_n = 1
133     for line in file.readlines():
134         obey(line.rstrip(), name + "file line " + str(line_n),
135              do_record=do_record)
136         line_n = line_n + 1
137     file.close()
138
139
140 def parse_command_line_arguments():
141     """Return settings values read from command line arguments."""
142     parser = argparse.ArgumentParser()
143     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
144                         action='store')
145     opts, unknown = parser.parse_known_args()
146     return opts
147
148
149 def server_test():
150     """Ensure valid server out file belonging to current process.
151
152     This is done by comparing io_db["teststring"] to what's found at the start
153     of the current file at io_db["path_out"]. On failure, set
154     io_db["kicked_by_rival"] and raise SystemExit.
155     """
156     if not os.access(io_db["path_out"], os.F_OK):
157         raise SystemExit("Server output file has disappeared.")
158     file = open(io_db["path_out"], "r")
159     test = file.readline().rstrip("\n")
160     file.close()
161     if test != io_db["teststring"]:
162         io_db["kicked_by_rival"] = True
163         msg = "Server test string in server output file does not match. This" \
164               " indicates that the current server process has been " \
165               "superseded by another one."
166         raise SystemExit(msg)
167
168
169 def read_command():
170     """Return next newline-delimited command from server in file.
171
172     Keep building return string until a newline is encountered. Pause between
173     unsuccessful reads, and after too much waiting, run server_test().
174     """
175     wait_on_fail = 1
176     max_wait = 5
177     now = time.time()
178     command = ""
179     while True:
180         add = io_db["file_in"].readline()
181         if len(add) > 0:
182             command = command + add
183             if len(command) > 0 and "\n" == command[-1]:
184                 command = command[:-1]
185                 break
186         else:
187             time.sleep(wait_on_fail)
188             if now + max_wait < time.time():
189                 server_test()
190                 now = time.time()
191     return command
192
193
194 def replay_game():
195     """Replay game from record file.
196
197     Use opts.replay as breakpoint turn to which to replay automatically before
198     switching to manual input by non-meta commands in server input file
199     triggering further reads of record file. Ensure opts.replay is at least 1.
200     """
201     if opts.replay < 1:
202         opts.replay = 1
203     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
204           " (if so late a turn is to be found).")
205     if not os.access(io_db["path_record"], os.F_OK):
206         raise SystemExit("No record file found to replay.")
207     io_db["file_record"] = open(io_db["path_record"], "r")
208     io_db["file_record"].prefix = "record file line "
209     io_db["file_record"].line_n = 1
210     while world_db["TURN"] < opts.replay:
211         line = io_db["file_record"].readline()
212         if "" == line:
213             break
214         obey(line.rstrip(), io_db["file_record"].prefix
215              + str(io_db["file_record"].line_n))
216         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
217     while True:
218         obey(read_command(), "in file", replay=True)
219
220
221 def play_game():
222     """Play game by server input file commands. Before, load save file found.
223
224     If no save file is found, a new world is generated from the commands in the
225     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
226     command and all that follow via the server input file.
227     """
228     if os.access(io_db["path_save"], os.F_OK):
229         obey_lines_in_file(io_db["path_save"], "save")
230     else:
231         if not os.access(io_db["path_worldconf"], os.F_OK):
232             msg = "No world config file from which to start a new world."
233             raise SystemExit(msg)
234         obey_lines_in_file(io_db["path_worldconf"], "world config ",
235                            do_record=True)
236         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
237     while True:
238         obey(read_command(), "in file", do_record=True)
239
240
241 def remake_map():
242     # DUMMY.
243     print("I'd (re-)make the map now, if only I knew how.")
244
245
246 def worlddb_value_setter(key, min, max):
247     """Generate: Set world_db[key] to int(val_string) if >= min and <= max."""
248     def func(val_string):
249         try:
250             val = int(val_string)
251             if val < min or val > max:
252                 raise ValueError
253             world_db[key] = val
254         except ValueError:
255             print("Ignoring: Please use integer >= " + str(min) + " and <= " +
256                   "str(max)+ '.")
257     return func
258
259
260 def command_ping():
261     """Send PONG line to server output file."""
262     io_db["file_out"].write("PONG\n")
263     io_db["file_out"].flush()
264
265
266 def command_quit():
267     """Abort server process."""
268     raise SystemExit("received QUIT command")
269
270
271 def command_seedmap(seed_string):
272     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
273     worlddb_value_setter("SEED_MAP", 0, 4294967295)(seed_string)
274     remake_map()
275
276
277 def command_makeworld(seed_string):
278     # Mere dummy so far.
279     worlddb_value_setter("SEED_MAP", 0, 4294967295)(seed_string)
280     worlddb_value_setter("SEED_RANDOMNESS", 0, 4294967295)(seed_string)
281
282
283 """Commands database.
284
285 Map command start tokens to ([0]) number of expected command arguments, ([1])
286 the command's meta-ness (i.e. is it to be written to the record file, is it to
287 be ignored in replay mode if read from server input file), and ([2]) a function
288 to be called on it.
289 """
290 commands_db = {
291     "QUIT": (0, True, command_quit),
292     "PING": (0, True, command_ping),
293     "MAKE_WORLD": (1, False, command_makeworld),
294     "SEED_MAP": (1, False, command_seedmap),
295     "SEED_RANDOMNESS": (1, False, worlddb_value_setter("SEED_RANDOMNESS", 0,
296                                                        4294967295)),
297     "TURN": (1, False, worlddb_value_setter("TURN", 0, 65535)),
298     "PLAYER_TYPE": (1, False, worlddb_value_setter("PLAYER_TYPE", 0, 255))
299 }
300
301
302 """World state database,"""
303 world_db = {
304     "TURN": 0,
305     "SEED_MAP": 0,
306     "SEED_RANDOMNESS": 0,
307     "PLAYER_TYPE": 0
308 }
309
310
311 """File IO database."""
312 io_db = {
313     "path_save": "save",
314     "path_record": "record",
315     "path_worldconf": "confserver/world",
316     "path_server": "server/",
317     "path_in": "server/in",
318     "path_out": "server/out",
319     "path_worldstate": "server/worldstate",
320     "tmp_suffix": "_tmp",
321     "kicked_by_rival": False
322 }
323
324
325 try:
326     opts = parse_command_line_arguments()
327     setup_server_io()
328     # print("DUMMY: Run game.")
329     if None != opts.replay:
330         replay_game()
331     else:
332         play_game()
333 except SystemExit as exit:
334     print("ABORTING: " + exit.args[0])
335 except:
336     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
337     raise
338 finally:
339     cleanup_server_io()
340     # print("DUMMY: (Clean up C heap.)")