10 """Fill IO files DB with proper file( path)s. Write process IO test string.
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"].
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):
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"])
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):
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()
54 def obey(command, prefix, replay=False, do_record=False):
55 """Call function from commands_db mapped to command's first token.
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.
65 print("input " + prefix + ": " + command)
67 tokens = shlex.split(command, comments=True)
68 except ValueError as err:
69 print("Can't tokenize command string: " + str(err) + ".")
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]()
76 print("Due to replay mode, reading command as 'go on in record'.")
77 line = io_db["file_record"].readline()
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
83 print("Reached end of record file.")
85 commands_db[tokens[0]][2](*tokens[1:])
90 print("Invalid command/argument, or bad number of tokens.")
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"]
99 if os.access(path, os.F_OK):
100 shutil.copyfile(path, path_tmp)
101 file = open(path_tmp, mode)
104 os.fsync(file.fileno())
106 if os.access(path, os.F_OK):
108 os.rename(path_tmp, path)
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)
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 "TURN " + str(world_db["TURN"]) + "\n" +
124 "SEED_RANDOMNESS " + str(world_db["SEED_RANDOMNESS"]) + "\n" +
125 "SEED_MAP " + str(world_db["SEED_MAP"]) + "\n")
128 def obey_lines_in_file(path, name, do_record=False):
129 """Call obey() on each line of path's file, use name in input prefix."""
130 file = open(path, "r")
132 for line in file.readlines():
133 obey(line.rstrip(), name + "file line " + str(line_n),
139 def parse_command_line_arguments():
140 """Return settings values read from command line arguments."""
141 parser = argparse.ArgumentParser()
142 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
144 opts, unknown = parser.parse_known_args()
149 """Ensure valid server out file belonging to current process.
151 This is done by comparing io_db["teststring"] to what's found at the start
152 of the current file at io_db["path_out"]. On failure, set
153 io_db["kicked_by_rival"] and raise SystemExit.
155 if not os.access(io_db["path_out"], os.F_OK):
156 raise SystemExit("Server output file has disappeared.")
157 file = open(io_db["path_out"], "r")
158 test = file.readline().rstrip("\n")
160 if test != io_db["teststring"]:
161 io_db["kicked_by_rival"] = True
162 msg = "Server test string in server output file does not match. This" \
163 " indicates that the current server process has been " \
164 "superseded by another one."
165 raise SystemExit(msg)
169 """Return next newline-delimited command from server in file.
171 Keep building return string until a newline is encountered. Pause between
172 unsuccessful reads, and after too much waiting, run server_test().
179 add = io_db["file_in"].readline()
181 command = command + add
182 if len(command) > 0 and "\n" == command[-1]:
183 command = command[:-1]
186 time.sleep(wait_on_fail)
187 if now + max_wait < time.time():
194 """Replay game from record file.
196 Use opts.replay as breakpoint turn to which to replay automatically before
197 switching to manual input by non-meta commands in server input file
198 triggering further reads of record file. Ensure opts.replay is at least 1.
202 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
203 " (if so late a turn is to be found).")
204 if not os.access(io_db["path_record"], os.F_OK):
205 raise SystemExit("No record file found to replay.")
206 io_db["file_record"] = open(io_db["path_record"], "r")
207 io_db["file_record"].prefix = "record file line "
208 io_db["file_record"].line_n = 1
209 while world_db["turn"] < opts.replay:
210 line = io_db["file_record"].readline()
213 obey(line.rstrip(), io_db["file_record"].prefix
214 + str(io_db["file_record"].line_n))
215 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
217 obey(read_command(), "in file", replay=True)
221 """Play game by server input file commands. Before, load save file found.
223 If no save file is found, a new world is generated from the commands in the
224 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
225 command and all that follow via the server input file.
227 if os.access(io_db["path_save"], os.F_OK):
228 obey_lines_in_file(io_db["path_save"], "save")
230 if not os.access(io_db["path_worldconf"], os.F_OK):
231 msg = "No world config file from which to start a new world."
232 raise SystemExit(msg)
233 obey_lines_in_file(io_db["path_worldconf"], "world config ",
235 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
237 obey(read_command(), "in file", do_record=True)
241 """Send PONG line to server output file."""
242 io_db["file_out"].write("PONG\n")
243 io_db["file_out"].flush()
247 """Abort server process."""
248 raise SystemExit("received QUIT command")
251 def command_turn(turn_string):
252 """Set turn to what's described in turn_string."""
254 turn = int(turn_string)
257 if turn < min or turn > max:
259 world_db["TURN"] = turn
261 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
265 def command_makeworld(seed_string):
268 seed = int(seed_string)
271 if seed < min or seed > max:
273 world_db["SEED_RANDOMNESS"] = seed
274 world_db["SEED_MAP"] = seed
276 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
280 """Commands database.
282 Map command start tokens to ([0]) minimum number of expected command arguments,
283 ([1]) the command's meta-ness (i.e. is it to be written to the record file, is
284 it to be ignored in replay mode if read from server input file), and ([2]) a
285 function to be called on it.
288 "QUIT": (0, True, command_quit),
289 "PING": (0, True, command_ping),
290 "MAKE_WORLD": (1, False, command_makeworld),
291 "TURN": (1, False, command_turn)
295 """World state database,"""
303 """File IO database."""
306 "path_record": "record",
307 "path_worldconf": "confserver/world",
308 "path_server": "server/",
309 "path_in": "server/in",
310 "path_out": "server/out",
311 "path_worldstate": "server/worldstate",
312 "tmp_suffix": "_tmp",
313 "kicked_by_rival": False
318 opts = parse_command_line_arguments()
320 # print("DUMMY: Run game.")
321 if None != opts.replay:
325 except SystemExit as exit:
326 print("ABORTING: " + exit.args[0])
328 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
332 # print("DUMMY: (Clean up C heap.)")