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. 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().
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]()
89 print("Invalid command/argument, or bad number of tokens.")
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")
103 os.fsync(file.fileno())
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"])
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")
114 for line in file.readlines():
115 obey(line.rstrip(), name + "file line " + str(line_n),
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,
126 opts, unknown = parser.parse_known_args()
131 """Ensure valid server out file belonging to current process.
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.
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")
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)
151 """Return next newline-delimited command from server in file.
153 Keep building return string until a newline is encountered. Pause between
154 unsuccessful reads, and after too much waiting, run server_test().
161 add = io_db["file_in"].readline()
163 command = command + add
164 if len(command) > 0 and "\n" == command[-1]:
165 command = command[:-1]
168 time.sleep(wait_on_fail)
169 if now + max_wait < time.time():
176 """Replay game from record file.
178 Use opts.replay as breakpoint turn to which to replay automatically before
179 switching to manual input by non-meta commands in server input file
180 triggering further reads of record file. Ensure opts.replay is at least 1.
184 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
185 " (if so late a turn is to be found).")
186 if not os.access(io_db["path_record"], os.F_OK):
187 raise SystemExit("No record file found to replay.")
188 io_db["file_record"] = open(io_db["path_record"], "r")
189 io_db["file_record"].prefix = "recod file line "
190 io_db["file_record"].line_n = 1
191 while world_db["turn"] < opts.replay:
192 line = io_db["file_record"].readline()
195 obey(line.rstrip(), io_db["file_record"].prefix
196 + str(io_db["file_record"].line_n))
197 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
199 obey(read_command(), "in file", replay=True)
203 """Play game by server input file commands. Before, load save file found.
205 If no save file is found, a new world is generated from the commands in the
206 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
207 command and all that follow via the server input file.
209 if os.access(io_db["path_save"], os.F_OK):
210 obey_lines_in_file(io_db["path_save"], "save")
212 if not os.access(io_db["path_worldconf"], os.F_OK):
213 msg = "No world config file from which to start a new world."
214 raise SystemExit(msg)
215 obey_lines_in_file(io_db["path_worldconf"], "world config ",
217 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
219 obey(read_command(), "in file", do_record=True)
222 def command_makeworld():
223 """Mere dummy so far."""
224 print("I would build a whole world now if only I knew how.")
228 """Send PONG line to server output file."""
229 io_db["file_out"].write("PONG\n")
230 io_db["file_out"].flush()
234 """Abort server process."""
235 raise SystemExit("received QUIT command")
238 """Commands database.
240 Map command start tokens to ([0]) minimum number of expected command arguments,
241 ([1]) the command's meta-ness (i.e. is it to be written to the record file, is
242 it to be ignored in replay mode if read from server input file), and ([2]) a
243 function to be called on it.
246 "QUIT": (0, True, command_quit),
247 "PING": (0, True, command_ping),
248 "MAKE_WORLD": (1, False, command_makeworld)
252 """World state database,"""
258 """File IO database."""
261 "path_record": "record",
262 "path_worldconf": "confserver/world",
263 "path_server": "server/",
264 "path_in": "server/in",
265 "path_out": "server/out",
266 "path_worldstate": "server/worldstate",
267 "tmp_suffix": "_tmp",
268 "kicked_by_rival": False
273 opts = parse_command_line_arguments()
275 # print("DUMMY: Run game.")
276 if None != opts.replay:
280 except SystemExit as exit:
281 print("ABORTING: " + exit.args[0])
283 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
287 # print("DUMMY: (Clean up C heap.)")