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