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 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)
182 for key in sorted(world_db.keys()):
183 if (not isinstance(world_db[key], dict) and
184 not isinstance(world_db[key], list)) and key != "MAP" and \
185 key != "WORLD_ACTIVE":
186 string = string + key + " " + str(world_db[key]) + "\n"
187 for plugin in world_db["PLUGIN"]:
188 string = string + "PLUGIN " + plugin + "\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)
210 def obey(command, prefix, replay=False, do_record=False):
211 """Call function from commands_db mapped to command's first token.
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
226 from server.config.commands import commands_db
229 print("input " + prefix + ": " + command)
231 tokens = shlex.split(command, comments=True)
232 except ValueError as err:
233 print("Can't tokenize command string: " + str(err) + ".")
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.")
242 print("Due to replay mode, reading command as 'go on in record'.")
243 line = io_db["file_record"].readline()
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
249 print("Reached end of record file.")
251 commands_db[tokens[0]][2](*tokens[1:])
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"],
257 if world_db["WORLD_ACTIVE"]:
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)
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")
270 for line in file.readlines():
271 obey(line.rstrip(), name + "file line " + str(line_n),
277 def try_worldstate_update():
278 """Write worldstate file if io_db["worldstate_updateable"] is set."""
279 if io_db["worldstate_updateable"]:
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":
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