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.
9 from server.config.world_data import world_db
10 from server.config.io import io_db
14 """Ensure valid server out file belonging to current process.
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.
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")
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."
33 def safely_remove_worldstate_file():
34 from server.io import server_test
35 if os.access(io_db["path_worldstate"], os.F_OK):
37 os.remove(io_db["path_worldstate"])
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"]
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)
52 if delete and os.access(path, os.F_OK):
54 os.rename(path_tmp, path)
57 def strong_write(file, string):
58 """Apply write(string), then flush()."""
63 def setup_server_io():
64 """Fill IO files DB with proper file( path)s. Write process IO test string.
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"].
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):
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"])
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):
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()
110 """Return next newline-delimited command from server in file.
112 Keep building return string until a newline is encountered. Pause between
113 unsuccessful reads, and after too much waiting, run server_test().
115 wait_on_fail = io_db["wait_on_read_fail"]
116 max_wait = io_db["max_wait_on_read_fail"]
120 add = io_db["file_in"].readline()
122 command = command + add
123 if len(command) > 0 and "\n" == command[-1]:
124 command = command[:-1]
127 time.sleep(wait_on_fail)
128 if now + max_wait < time.time():
135 """Send "msg" to log."""
136 strong_write(io_db["file_out"], "LOG " + msg + "\n")
140 """Save all commands needed to reconstruct current world state."""
141 from server.utils import rand
143 def quote_escape(string):
144 string = string.replace("\u005C", '\u005C\u005C')
145 return '"' + string.replace('"', '\u005C"') + '"'
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"
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"
168 def helper(category, id_string, special_keys={}):
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"
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"}:
190 string += key + " " + (quote_escape(argument) if \
191 str == type(argument) else str(argument)) + "\n"
192 string += memthing(tid) + memmap(tid) + memdepthmap(tid)
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)
220 def obey(command, prefix, replay=False, do_record=False):
221 """Call function from commands_db mapped to command's first token.
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
236 from server.config.commands import commands_db
239 print("input " + prefix + ": " + command)
241 tokens = shlex.split(command, comments=True)
242 except ValueError as err:
243 print("Can't tokenize command string: " + str(err) + ".")
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.")
252 print("Due to replay mode, reading command as 'go on in record'.")
253 line = io_db["file_record"].readline()
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
259 print("Reached end of record file.")
261 commands_db[tokens[0]][2](*tokens[1:])
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"],
267 if world_db["WORLD_ACTIVE"]:
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)
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")
280 for line in file.readlines():
281 obey(line.rstrip(), name + "file line " + str(line_n),
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"]:
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":
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