home · contact · privacy
Add GPL notices to all source files.
[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     string = ""
182     for key in sorted(world_db.keys()):
183         if (not isinstance(world_db[key], dict)) and key != "MAP" and \
184            key != "WORLD_ACTIVE":
185             string = string + key + " " + str(world_db[key]) + "\n"
186     string = string + mapsetter("MAP")()
187     string = string + helper("ThingActions", "TA_ID")
188     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
189     for id in sorted(world_db["ThingTypes"].keys()):
190         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
191             str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
192     string = string + helper("Things", "T_ID",
193                              {"T_CARRIES": False, "carried": False,
194                               "T_MEMMAP": mapsetter("T_MEMMAP"),
195                               "T_MEMTHING": memthing, "fovmap": False,
196                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
197     for id in sorted(world_db["Things"].keys()):
198         if [] != world_db["Things"][id]["T_CARRIES"]:
199             string = string + "T_ID " + str(id) + "\n"
200             for carried in sorted(world_db["Things"][id]["T_CARRIES"]):
201                 string = string + "T_CARRIES " + str(carried) + "\n"
202     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
203         "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
204     atomic_write(io_db["path_save"], string)
205
206
207 def obey(command, prefix, replay=False, do_record=False):
208     """Call function from commands_db mapped to command's first token.
209
210     Tokenize command string with shlex.split(comments=True). If replay is set,
211     a non-meta command from the commands_db merely triggers obey() on the next
212     command from the records file. If not, non-meta commands set
213     io_db["worldstate_updateable"] to world_db["WORLD_ACTIVE"], and, if
214     do_record is set, are recorded to io_db["record_chunk"], and save_world()
215     is called (and io_db["record_chunk"] written) if io_db["save_wait"] seconds
216     have passed since the last time it was called. The prefix string is
217     inserted into the server's input message between its beginning 'input ' and
218     ':'. All activity is preceded by a server_test() call. Commands that start
219     with a lowercase letter are ignored when world_db["WORLD_ACTIVE"] is
220     False/0.
221     """
222     import shlex
223     from server.config.commands import commands_db
224     server_test()
225     if io_db["verbose"]:
226         print("input " + prefix + ": " + command)
227     try:
228         tokens = shlex.split(command, comments=True)
229     except ValueError as err:
230         print("Can't tokenize command string: " + str(err) + ".")
231         return
232     if len(tokens) > 0 and tokens[0] in commands_db \
233        and len(tokens) == commands_db[tokens[0]][0] + 1:
234         if commands_db[tokens[0]][1]:
235             commands_db[tokens[0]][2](*tokens[1:])
236         elif tokens[0][0].islower() and not world_db["WORLD_ACTIVE"]:
237             print("Ignoring lowercase-starting commands when world inactive.")
238         elif replay:
239             print("Due to replay mode, reading command as 'go on in record'.")
240             line = io_db["file_record"].readline()
241             if len(line) > 0:
242                 obey(line.rstrip(), io_db["file_record"].prefix
243                      + str(io_db["file_record"].line_n))
244                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
245             else:
246                 print("Reached end of record file.")
247         else:
248             commands_db[tokens[0]][2](*tokens[1:])
249             if do_record:
250                 io_db["record_chunk"] += command + "\n"
251                 if time.time() > io_db["save_wait_start"] + io_db["save_wait"]:
252                     atomic_write(io_db["path_record"], io_db["record_chunk"],
253                                  do_append=True)
254                     if world_db["WORLD_ACTIVE"]:
255                         save_world()
256                     io_db["record_chunk"] = ""
257                     io_db["save_wait"] = time.time()
258             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
259     elif 0 != len(tokens):
260         print("Invalid command/argument, or bad number of tokens.")
261
262
263 def obey_lines_in_file(path, name, do_record=False):
264     """Call obey() on each line of path's file, use name in input prefix."""
265     file = open(path, "r")
266     line_n = 1
267     for line in file.readlines():
268         obey(line.rstrip(), name + "file line " + str(line_n),
269              do_record=do_record)
270         line_n = line_n + 1
271     file.close()
272
273
274 def try_worldstate_update():
275     """Write worldstate file if io_db["worldstate_updateable"] is set."""
276     from server.config.commands import commands_db
277     if io_db["worldstate_updateable"]:
278
279         def write_map(string, map):
280             for i in range(length):
281                 line = map[i * length:(i * length) + length].decode()
282                 string = string + line + "\n"
283             return string
284
285         inventory = ""
286         if [] == world_db["Things"][0]["T_CARRIES"]:
287             inventory = "(none)\n"
288         else:
289             for id in world_db["Things"][0]["T_CARRIES"]:
290                 type_id = world_db["Things"][id]["T_TYPE"]
291                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
292                 inventory = inventory + name + "\n"
293         string = str(world_db["TURN"]) + "\n" + \
294             str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
295             str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
296             inventory + "%\n" + \
297             str(world_db["Things"][0]["T_POSY"]) + "\n" + \
298             str(world_db["Things"][0]["T_POSX"]) + "\n" + \
299             str(world_db["MAP_LENGTH"]) + "\n"
300         length = world_db["MAP_LENGTH"]
301         fov = bytearray(b' ' * (length ** 2))
302         ord_v = ord("v")
303         for pos in [pos for pos in range(length ** 2)
304                         if ord_v == world_db["Things"][0]["fovmap"][pos]]:
305             fov[pos] = world_db["MAP"][pos]
306         length = world_db["MAP_LENGTH"]
307         for id in [id for tid in reversed(sorted(list(world_db["ThingTypes"])))
308                       for id in world_db["Things"]
309                       if not world_db["Things"][id]["carried"]
310                       if world_db["Things"][id]["T_TYPE"] == tid
311                       if world_db["Things"][0]["fovmap"][
312                            world_db["Things"][id]["T_POSY"] * length
313                            + world_db["Things"][id]["T_POSX"]] == ord_v]:
314             type = world_db["Things"][id]["T_TYPE"]
315             c = ord(world_db["ThingTypes"][type]["TT_SYMBOL"])
316             fov[world_db["Things"][id]["T_POSY"] * length
317                 + world_db["Things"][id]["T_POSX"]] = c
318         string = write_map(string, fov)
319         mem = world_db["Things"][0]["T_MEMMAP"][:]
320         for mt in [mt for tid in reversed(sorted(list(world_db["ThingTypes"])))
321                       for mt in world_db["Things"][0]["T_MEMTHING"]
322                       if mt[0] == tid]:
323              c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
324              mem[(mt[1] * length) + mt[2]] = ord(c)
325         string = write_map(string, mem)
326         atomic_write(io_db["path_worldstate"], string, delete=False)
327         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
328         io_db["worldstate_updateable"] = False