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 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)
207 def obey(command, prefix, replay=False, do_record=False):
208 """Call function from commands_db mapped to command's first token.
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
223 from server.config.commands import commands_db
226 print("input " + prefix + ": " + command)
228 tokens = shlex.split(command, comments=True)
229 except ValueError as err:
230 print("Can't tokenize command string: " + str(err) + ".")
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.")
239 print("Due to replay mode, reading command as 'go on in record'.")
240 line = io_db["file_record"].readline()
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
246 print("Reached end of record file.")
248 commands_db[tokens[0]][2](*tokens[1:])
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"],
254 if world_db["WORLD_ACTIVE"]:
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.")
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")
267 for line in file.readlines():
268 obey(line.rstrip(), name + "file line " + str(line_n),
274 def try_worldstate_update():
275 """Write worldstate file if io_db["worldstate_updateable"] is set."""
276 if io_db["worldstate_updateable"]:
278 for entry in io_db["worldstate_write_order"]:
279 if entry[1] == "world_int":
280 string += str(world_db[entry[0]]) + "\n"
281 elif entry[1] == "player_int":
282 string += str(world_db["Things"][0][entry[0]]) + "\n"
283 elif entry[1] == "func":
285 atomic_write(io_db["path_worldstate"], string, delete=False)
286 strong_write(io_db["file_out"], "WORLD_UPDATED\n")
287 io_db["worldstate_updateable"] = False