home · contact · privacy
Server: Split in many files, reorganize code slightly to fit change.
[plomrogue] / server / io.py
1 import os
2 import time
3
4
5 from server.config.world_data import world_db
6 from server.config.io import io_db
7
8
9 def server_test():
10     """Ensure valid server out file belonging to current process.
11
12     This is done by comparing io_db["teststring"] to what's found at the start
13     of the current file at io_db["path_out"]. On failure, set
14     io_db["kicked_by_rival"] and raise SystemExit.
15     """
16     if not os.access(io_db["path_out"], os.F_OK):
17         raise SystemExit("Server output file has disappeared.")
18     file = open(io_db["path_out"], "r")
19     test = file.readline().rstrip("\n")
20     file.close()
21     if test != io_db["teststring"]:
22         io_db["kicked_by_rival"] = True
23         msg = "Server test string in server output file does not match. This" \
24               " indicates that the current server process has been " \
25               "superseded by another one."
26         raise SystemExit(msg)
27
28
29 def safely_remove_worldstate_file():
30     from server.io import server_test
31     if os.access(io_db["path_worldstate"], os.F_OK):
32         server_test()
33         os.remove(io_db["path_worldstate"])
34
35
36 def atomic_write(path, text, do_append=False, delete=True):
37     """Atomic write of text to file at path, appended if do_append is set."""
38     path_tmp = path + io_db["tmp_suffix"]
39     mode = "w"
40     if do_append:
41         mode = "a"
42         if os.access(path, os.F_OK):
43             from shutil import copyfile 
44             copyfile(path, path_tmp)
45     file = open(path_tmp, mode)
46     strong_write(file, text)
47     file.close()
48     if delete and os.access(path, os.F_OK):
49         os.remove(path)
50     os.rename(path_tmp, path)
51
52
53 def strong_write(file, string):
54     """Apply write(string), then flush()."""
55     file.write(string)
56     file.flush()
57
58
59 def setup_server_io():
60     """Fill IO files DB with proper file( path)s. Write process IO test string.
61
62     Ensure IO files directory at server/. Remove any old input file if found.
63     Set up new input file for reading, and new output file for appending. Start
64     output file with process hash line of format PID + " " + floated UNIX time
65     (io_db["teststring"]). Raise SystemExit if file is found at path of either
66     record or save file plus io_db["tmp_suffix"].
67     """
68     def detect_atomic_leftover(path, tmp_suffix):
69         path_tmp = path + tmp_suffix
70         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
71               "aborted previous attempt to write '" + path + "'. Aborting " \
72               "until matter is resolved by removing it from its current path."
73         if os.access(path_tmp, os.F_OK):
74             raise SystemExit(msg)
75     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
76     io_db["save_wait_start"] = 0
77     io_db["verbose"] = False
78     io_db["record_chunk"] = ""
79     os.makedirs(io_db["path_server"], exist_ok=True)
80     io_db["file_out"] = open(io_db["path_out"], "a")
81     strong_write(io_db["file_out"], io_db["teststring"] + "\n")
82     if os.access(io_db["path_in"], os.F_OK):
83         os.remove(io_db["path_in"])
84     io_db["file_in"] = open(io_db["path_in"], "w")
85     io_db["file_in"].close()
86     io_db["file_in"] = open(io_db["path_in"], "r")
87     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
88     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
89
90
91 def cleanup_server_io():
92     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
93     def helper(file_key, path_key):
94         if file_key in io_db:
95             io_db[file_key].close()
96         if not io_db["kicked_by_rival"] \
97            and os.access(io_db[path_key], os.F_OK):
98             os.remove(io_db[path_key])
99     helper("file_in", "path_in")
100     helper("file_out", "path_out")
101     helper("file_worldstate", "path_worldstate")
102     if "file_record" in io_db:
103         io_db["file_record"].close()
104
105 def read_command():
106     """Return next newline-delimited command from server in file.
107
108     Keep building return string until a newline is encountered. Pause between
109     unsuccessful reads, and after too much waiting, run server_test().
110     """
111     wait_on_fail = io_db["wait_on_read_fail"]
112     max_wait = io_db["max_wait_on_read_fail"] 
113     now = time.time()
114     command = ""
115     while True:
116         add = io_db["file_in"].readline()
117         if len(add) > 0:
118             command = command + add
119             if len(command) > 0 and "\n" == command[-1]:
120                 command = command[:-1]
121                 break
122         else:
123             time.sleep(wait_on_fail)
124             if now + max_wait < time.time():
125                 server_test()
126                 now = time.time()
127     return command
128
129
130 def log(msg):
131     """Send "msg" to log."""
132     strong_write(io_db["file_out"], "LOG " + msg + "\n")
133
134
135 def save_world():
136     """Save all commands needed to reconstruct current world state."""
137     from server.utils import rand
138
139     def quote_escape(string):
140         string = string.replace("\u005C", '\u005C\u005C')
141         return '"' + string.replace('"', '\u005C"') + '"'
142
143     def mapsetter(key):
144         def helper(id=None):
145             string = ""
146             if key == "MAP" or world_db["Things"][id][key]:
147                 map = world_db["MAP"] if key == "MAP" \
148                                       else world_db["Things"][id][key]
149                 length = world_db["MAP_LENGTH"]
150                 for i in range(length):
151                     line = map[i * length:(i * length) + length].decode()
152                     string = string + key + " " + str(i) + " " + \
153                         quote_escape(line) + "\n"
154             return string
155         return helper
156
157     def memthing(id):
158         string = ""
159         for memthing in world_db["Things"][id]["T_MEMTHING"]:
160             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
161                 str(memthing[1]) + " " + str(memthing[2]) + "\n"
162         return string
163
164     def helper(category, id_string, special_keys={}):
165         string = ""
166         for id in sorted(world_db[category].keys()):
167             string = string + id_string + " " + str(id) + "\n"
168             for key in sorted(world_db[category][id].keys()):
169                 if not key in special_keys:
170                     x = world_db[category][id][key]
171                     argument = quote_escape(x) if str == type(x) else str(x)
172                     string = string + key + " " + argument + "\n"
173                 elif special_keys[key]:
174                     string = string + special_keys[key](id)
175         return string
176
177     string = ""
178     for key in sorted(world_db.keys()):
179         if (not isinstance(world_db[key], dict)) and key != "MAP" and \
180            key != "WORLD_ACTIVE":
181             string = string + key + " " + str(world_db[key]) + "\n"
182     string = string + mapsetter("MAP")()
183     string = string + helper("ThingActions", "TA_ID")
184     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
185     for id in sorted(world_db["ThingTypes"].keys()):
186         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
187             str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
188     string = string + helper("Things", "T_ID",
189                              {"T_CARRIES": False, "carried": False,
190                               "T_MEMMAP": mapsetter("T_MEMMAP"),
191                               "T_MEMTHING": memthing, "fovmap": False,
192                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
193     for id in sorted(world_db["Things"].keys()):
194         if [] != world_db["Things"][id]["T_CARRIES"]:
195             string = string + "T_ID " + str(id) + "\n"
196             for carried in sorted(world_db["Things"][id]["T_CARRIES"]):
197                 string = string + "T_CARRIES " + str(carried) + "\n"
198     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
199         "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
200     atomic_write(io_db["path_save"], string)
201
202
203 def obey(command, prefix, replay=False, do_record=False):
204     """Call function from commands_db mapped to command's first token.
205
206     Tokenize command string with shlex.split(comments=True). If replay is set,
207     a non-meta command from the commands_db merely triggers obey() on the next
208     command from the records file. If not, non-meta commands set
209     io_db["worldstate_updateable"] to world_db["WORLD_ACTIVE"], and, if
210     do_record is set, are recorded to io_db["record_chunk"], and save_world()
211     is called (and io_db["record_chunk"] written) if io_db["save_wait"] seconds
212     have passed since the last time it was called. The prefix string is
213     inserted into the server's input message between its beginning 'input ' and
214     ':'. All activity is preceded by a server_test() call. Commands that start
215     with a lowercase letter are ignored when world_db["WORLD_ACTIVE"] is
216     False/0.
217     """
218     import shlex
219     from server.config.commands import commands_db
220     server_test()
221     if io_db["verbose"]:
222         print("input " + prefix + ": " + command)
223     try:
224         tokens = shlex.split(command, comments=True)
225     except ValueError as err:
226         print("Can't tokenize command string: " + str(err) + ".")
227         return
228     if len(tokens) > 0 and tokens[0] in commands_db \
229        and len(tokens) == commands_db[tokens[0]][0] + 1:
230         if commands_db[tokens[0]][1]:
231             commands_db[tokens[0]][2](*tokens[1:])
232         elif tokens[0][0].islower() and not world_db["WORLD_ACTIVE"]:
233             print("Ignoring lowercase-starting commands when world inactive.")
234         elif replay:
235             print("Due to replay mode, reading command as 'go on in record'.")
236             line = io_db["file_record"].readline()
237             if len(line) > 0:
238                 obey(line.rstrip(), io_db["file_record"].prefix
239                      + str(io_db["file_record"].line_n))
240                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
241             else:
242                 print("Reached end of record file.")
243         else:
244             commands_db[tokens[0]][2](*tokens[1:])
245             if do_record:
246                 io_db["record_chunk"] += command + "\n"
247                 if time.time() > io_db["save_wait_start"] + io_db["save_wait"]:
248                     atomic_write(io_db["path_record"], io_db["record_chunk"],
249                                  do_append=True)
250                     if world_db["WORLD_ACTIVE"]:
251                         save_world()
252                     io_db["record_chunk"] = ""
253                     io_db["save_wait"] = time.time()
254             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
255     elif 0 != len(tokens):
256         print("Invalid command/argument, or bad number of tokens.")
257
258
259 def obey_lines_in_file(path, name, do_record=False):
260     """Call obey() on each line of path's file, use name in input prefix."""
261     file = open(path, "r")
262     line_n = 1
263     for line in file.readlines():
264         obey(line.rstrip(), name + "file line " + str(line_n),
265              do_record=do_record)
266         line_n = line_n + 1
267     file.close()
268
269
270 def try_worldstate_update():
271     """Write worldstate file if io_db["worldstate_updateable"] is set."""
272     from server.config.commands import commands_db
273     if io_db["worldstate_updateable"]:
274
275         def write_map(string, map):
276             for i in range(length):
277                 line = map[i * length:(i * length) + length].decode()
278                 string = string + line + "\n"
279             return string
280
281         inventory = ""
282         if [] == world_db["Things"][0]["T_CARRIES"]:
283             inventory = "(none)\n"
284         else:
285             for id in world_db["Things"][0]["T_CARRIES"]:
286                 type_id = world_db["Things"][id]["T_TYPE"]
287                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
288                 inventory = inventory + name + "\n"
289         string = str(world_db["TURN"]) + "\n" + \
290             str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
291             str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
292             inventory + "%\n" + \
293             str(world_db["Things"][0]["T_POSY"]) + "\n" + \
294             str(world_db["Things"][0]["T_POSX"]) + "\n" + \
295             str(world_db["MAP_LENGTH"]) + "\n"
296         length = world_db["MAP_LENGTH"]
297         fov = bytearray(b' ' * (length ** 2))
298         ord_v = ord("v")
299         for pos in [pos for pos in range(length ** 2)
300                         if ord_v == world_db["Things"][0]["fovmap"][pos]]:
301             fov[pos] = world_db["MAP"][pos]
302         length = world_db["MAP_LENGTH"]
303         for id in [id for tid in reversed(sorted(list(world_db["ThingTypes"])))
304                       for id in world_db["Things"]
305                       if not world_db["Things"][id]["carried"]
306                       if world_db["Things"][id]["T_TYPE"] == tid
307                       if world_db["Things"][0]["fovmap"][
308                            world_db["Things"][id]["T_POSY"] * length
309                            + world_db["Things"][id]["T_POSX"]] == ord_v]:
310             type = world_db["Things"][id]["T_TYPE"]
311             c = ord(world_db["ThingTypes"][type]["TT_SYMBOL"])
312             fov[world_db["Things"][id]["T_POSY"] * length
313                 + world_db["Things"][id]["T_POSX"]] = c
314         string = write_map(string, fov)
315         mem = world_db["Things"][0]["T_MEMMAP"][:]
316         for mt in [mt for tid in reversed(sorted(list(world_db["ThingTypes"])))
317                       for mt in world_db["Things"][0]["T_MEMTHING"]
318                       if mt[0] == tid]:
319              c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
320              mem[(mt[1] * length) + mt[2]] = ord(c)
321         string = write_map(string, mem)
322         atomic_write(io_db["path_worldstate"], string, delete=False)
323         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
324         io_db["worldstate_updateable"] = False