home · contact · privacy
Server: Don't save world_db entries with keys that start non-uppercase.
[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 plugin in world_db["PLUGIN"]:
183         string = string + "PLUGIN " + plugin + "\n"
184     for key in sorted(world_db.keys()):
185         if (not isinstance(world_db[key], dict) and
186             not isinstance(world_db[key], list)) and key != "MAP" and \
187            key != "WORLD_ACTIVE" and key[0].isupper():
188             string = string + key + " " + str(world_db[key]) + "\n"
189     string = string + mapsetter("MAP")()
190     string = string + helper("ThingActions", "TA_ID")
191     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
192     for id in sorted(world_db["ThingTypes"].keys()):
193         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
194             str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
195     string = string + helper("Things", "T_ID",
196                              {"T_CARRIES": False, "carried": False,
197                               "T_MEMMAP": mapsetter("T_MEMMAP"),
198                               "T_MEMTHING": memthing, "fovmap": False,
199                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
200     for id in sorted(world_db["Things"].keys()):
201         if [] != world_db["Things"][id]["T_CARRIES"]:
202             string = string + "T_ID " + str(id) + "\n"
203             for carried in sorted(world_db["Things"][id]["T_CARRIES"]):
204                 string = string + "T_CARRIES " + str(carried) + "\n"
205     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
206         "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
207     atomic_write(io_db["path_save"], string)
208
209
210 def obey(command, prefix, replay=False, do_record=False):
211     """Call function from commands_db mapped to command's first token.
212
213     Tokenize command string with shlex.split(comments=True). If replay is set,
214     a non-meta command from the commands_db merely triggers obey() on the next
215     command from the records file. If not, non-meta commands set
216     io_db["worldstate_updateable"] to world_db["WORLD_ACTIVE"], and, if
217     do_record is set, are recorded to io_db["record_chunk"], and save_world()
218     is called (and io_db["record_chunk"] written) if io_db["save_wait"] seconds
219     have passed since the last time it was called. The prefix string is
220     inserted into the server's input message between its beginning 'input ' and
221     ':'. All activity is preceded by a server_test() call. Commands that start
222     with a lowercase letter are ignored when world_db["WORLD_ACTIVE"] is
223     False/0.
224     """
225     import shlex
226     from server.config.commands import commands_db
227     server_test()
228     if io_db["verbose"]:
229         print("input " + prefix + ": " + command)
230     try:
231         tokens = shlex.split(command, comments=True)
232     except ValueError as err:
233         print("Can't tokenize command string: " + str(err) + ".")
234         return
235     if len(tokens) > 0 and tokens[0] in commands_db \
236        and len(tokens) == commands_db[tokens[0]][0] + 1:
237         if commands_db[tokens[0]][1]:
238             commands_db[tokens[0]][2](*tokens[1:])
239         elif tokens[0][0].islower() and not world_db["WORLD_ACTIVE"]:
240             print("Ignoring lowercase-starting commands when world inactive.")
241         elif replay:
242             print("Due to replay mode, reading command as 'go on in record'.")
243             line = io_db["file_record"].readline()
244             if len(line) > 0:
245                 obey(line.rstrip(), io_db["file_record"].prefix
246                      + str(io_db["file_record"].line_n))
247                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
248             else:
249                 print("Reached end of record file.")
250         else:
251             commands_db[tokens[0]][2](*tokens[1:])
252             if do_record:
253                 io_db["record_chunk"] += command + "\n"
254                 if time.time() > io_db["save_wait_start"] + io_db["save_wait"]:
255                     atomic_write(io_db["path_record"], io_db["record_chunk"],
256                                  do_append=True)
257                     if world_db["WORLD_ACTIVE"]:
258                         save_world()
259                     io_db["record_chunk"] = ""
260                     io_db["save_wait"] = time.time()
261             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
262     elif 0 != len(tokens):
263         print("Invalid command/argument, or bad number of tokens: " + command)
264
265
266 def obey_lines_in_file(path, name, do_record=False):
267     """Call obey() on each line of path's file, use name in input prefix."""
268     file = open(path, "r")
269     line_n = 1
270     for line in file.readlines():
271         obey(line.rstrip(), name + "file line " + str(line_n),
272              do_record=do_record)
273         line_n = line_n + 1
274     file.close()
275
276
277 def try_worldstate_update():
278     """Write worldstate file if io_db["worldstate_updateable"] is set."""
279     if io_db["worldstate_updateable"]:
280         string = ""
281         for entry in io_db["worldstate_write_order"]:
282             if entry[1] == "world_int":
283                 string += str(world_db[entry[0]]) + "\n"
284             elif entry[1] == "player_int":
285                 string += str(world_db["Things"][0][entry[0]]) + "\n"
286             elif entry[1] == "func":
287                 string += entry[0]()
288         atomic_write(io_db["path_worldstate"], string, delete=False)
289         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
290         io_db["worldstate_updateable"] = False