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