home · contact · privacy
Server/py: Fix variable naming bug.
[plomrogue] / plomrogue-server.py
1 import argparse
2 import errno
3 import os
4 import shlex
5 import shutil
6 import time
7
8
9 def setup_server_io():
10     """Fill IO files DB with proper file( path)s. Write process IO test string.
11
12     Set io_db["kicked_by_rival"] to False. Decide file paths. Ensure IO files
13     directory at server/. Remove any old in file if found. Set up new in file
14     (io_db["file_in"]) for reading at io_db["path_in"], and new out file
15     (io_db["file_out"]) for writing at io_db["path_out"]. Start out file with
16     process hash line of format PID + " " + floated UNIX time
17     (io_db["teststring"]). Run detect_atomic_leftover on io_db["path_record"]
18     and io_db["path_save"].
19     """
20     io_dir = "server/"
21     io_db["kicked_by_rival"] = False
22     io_db["path_in"] = io_dir + "in"
23     io_db["path_out"] = io_dir + "out"
24     io_db["path_worldstate"] = io_dir + "worldstate"
25     io_db["path_record"] = "record"
26     io_db["path_save"] = "save"
27     io_db["path_worldconf"] = "confserver/world"
28     io_db["tmp_suffix"] = "_tmp"
29     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
30     os.makedirs(io_dir, exist_ok=True)
31     io_db["file_out"] = open(io_db["path_out"], "w")
32     io_db["file_out"].write(io_db["teststring"] + "\n")
33     io_db["file_out"].flush()
34     if os.access(io_db["path_in"], os.F_OK):
35         os.remove(io_db["path_in"])
36     io_db["file_in"] = open(io_db["path_in"], "w")
37     io_db["file_in"].close()
38     io_db["file_in"] = open(io_db["path_in"], "r")
39     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
40     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
41
42
43 def cleanup_server_io():
44     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
45     def helper(file_key, path_key):
46         if file_key in io_db:
47             io_db[file_key].close()
48             if not io_db["kicked_by_rival"] \
49                and os.access(io_db[path_key], os.F_OK):
50                 os.remove(io_db[path_key])
51     helper("file_out", "path_out")
52     helper("file_in", "path_in")
53     helper("file_worldstate", "path_worldstate")
54     if "file_record" in io_db:
55         io_db["file_record"].close()
56
57
58 def detect_atomic_leftover(path, tmp_suffix):
59     """Raise explained SystemExit if file is found at path + tmp_suffix."""
60     path_tmp = path + tmp_suffix
61     msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
62           "aborted previous attempt to write '" + path + "'. Aborting until " \
63           "the matter is resolved by removing it from its current path."
64     if os.access(path_tmp, os.F_OK):
65         raise SystemExit(msg)
66
67
68 def obey(command, prefix, replay=False, do_record=False):
69     """Call function from commands_db mapped to command's first token.
70
71     The command string is tokenized by shlex.split(comments=True). If replay is
72     set, a non-meta command from the commands_db merely triggers obey() on the
73     next command from the records file. Non-meta commands are recorded in
74     non-replay mode if do_record is set. The prefix string is inserted into the
75     server's input message between its beginning 'input ' and ':'. All activity
76     is preceded by a call to server_test().
77     """
78     server_test()
79     print("input " + prefix + ": " + command)
80     try:
81         tokens = shlex.split(command, comments=True)
82     except ValueError as err:
83         print("Can't tokenize command string: " + str(err) + ".")
84         return
85     if len(tokens) > 0 and tokens[0] in commands_db \
86        and len(tokens) >= commands_db[tokens[0]][0] + 1:
87         if commands_db[tokens[0]][1]:
88             commands_db[tokens[0]][2]()
89         elif replay:
90             print("Due to replay mode, reading command as 'go on in record'.")
91             line = io_db["file_record"].readline()
92             if len(line) > 0:
93                 obey(line.rstrip(), io_db["file_record"].prefix
94                      + str(io_db["file_record"].line_n))
95                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
96             else:
97                 print("Reached end of record file.")
98         else:
99             commands_db[tokens[0]][2]()
100             if do_record:
101                 record(command)
102     else:
103         print("Invalid command/argument, or bad number of tokens.")
104
105
106 def record(command):
107     """Append command string plus newline to record file. (Atomic.)"""
108     # This misses some optimizations from the original record(), namely only
109     # finishing the atomic write with expensive flush() and fsync() every 15
110     # seconds unless explicitely forced. Implement as needed.
111     path_tmp = io_db["path_record"] + io_db["tmp_suffix"]
112     if os.access(io_db["path_record"], os.F_OK):
113         shutil.copyfile(io_db["path_record"], path_tmp)
114     file = open(path_tmp, "a")
115     file.write(command + "\n")
116     file.flush()
117     os.fsync(file.fileno())
118     file.close()
119     if os.access(io_db["path_record"], os.F_OK):
120         os.remove(io_db["path_record"])
121     os.rename(path_tmp, io_db["path_record"])
122
123
124 def obey_lines_in_file(path, name, do_record=False):
125     """Call obey() on each line of path's file, use name in input prefix."""
126     file = open(path, "r")
127     line_n = 1
128     for line in file.readlines():
129         obey(line.rstrip(), name + "file line " + str(line_n),
130              do_record=do_record)
131         line_n = line_n + 1
132     file.close()
133
134
135 def parse_command_line_arguments():
136     """Return settings values read from command line arguments."""
137     parser = argparse.ArgumentParser()
138     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
139                         action='store')
140     opts, unknown = parser.parse_known_args()
141     return opts
142
143
144 def server_test():
145     """Ensure valid server out file belonging to current process.
146
147     On failure, set io_db["kicked_by_rival"] and raise SystemExit.
148     """
149     if not os.access(io_db["path_out"], os.F_OK):
150         raise SystemExit("Server output file has disappeared.")
151     file = open(io_db["path_out"], "r")
152     test = file.readline().rstrip("\n")
153     file.close()
154     if test != io_db["teststring"]:
155         io_db["kicked_by_rival"] = True
156         msg = "Server test string in server output file does not match. This" \
157               " indicates that the current server process has been " \
158               "superseded by another one."
159         raise SystemExit(msg)
160
161
162 def read_command():
163     """Return next newline-delimited command from server in file.
164
165     Keep building return string until a newline is encountered. Pause between
166     unsuccessful reads, and after too much waiting, run server_test().
167     """
168     wait_on_fail = 1
169     max_wait = 5
170     now = time.time()
171     command = ""
172     while 1:
173         add = io_db["file_in"].readline()
174         if len(add) > 0:
175             command = command + add
176             if len(command) > 0 and "\n" == command[-1]:
177                 command = command[:-1]
178                 break
179         else:
180             time.sleep(wait_on_fail)
181             if now + max_wait < time.time():
182                 server_test()
183                 now = time.time()
184     return command
185
186
187 def command_makeworld():
188     """Mere dummy so far."""
189     print("I would build a whole world now if only I knew how.")
190
191
192 def command_ping():
193     """Send PONG line to server output file."""
194     io_db["file_out"].write("PONG\n")
195     io_db["file_out"].flush()
196
197
198 def command_quit():
199     """Abort server process."""
200     raise SystemExit("received QUIT command")
201
202
203 """Commands database.
204
205 Map command start tokens to ([0]) minimum number of expected command arguments,
206 ([1]) the command's meta-ness (i.e. is it to be written to the record file, is
207 it to be ignored in replay mode if read from server input file), and ([2]) a
208 function to be called on it.
209 """
210 commands_db = {
211     "QUIT": (0, True, command_quit),
212     "PING": (0, True, command_ping),
213     "MAKE_WORLD": (1, False, command_makeworld)
214 }
215
216 io_db = {}
217 world_db = {}
218 try:
219     opts = parse_command_line_arguments()
220     setup_server_io()
221     # print("DUMMY: Run game.")
222     if None != opts.replay:
223         if opts.replay < 1:
224             opts.replay = 1
225         print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
226               " (if so late a turn is to be found).")
227         if not os.access(io_db["path_record"], os.F_OK):
228             raise SystemExit("No record file found to replay.")
229         world_db["turn"] = 0
230         io_db["file_record"] = open(io_db["path_record"], "r")
231         io_db["file_record"].prefix = "recod file line "
232         io_db["file_record"].line_n = 1
233         while world_db["turn"] < opts.replay:
234             line = io_db["file_record"].readline()
235             if "" == line:
236                 break
237             obey(line.rstrip(), io_db["file_record"].prefix
238                  + str(io_db["file_record"].line_n))
239             io_db["file_record"].line_n = io_db["file_record"].line_n + 1
240         while True:
241             obey(read_command(), "in file", replay=True)
242     else:
243         if os.access(io_db["path_save"], os.F_OK):
244             obey_lines_in_file(io_db["path_save"], "save")
245         else:
246             if not os.access(io_db["path_worldconf"], os.F_OK):
247                 msg = "No world config file from which to start a new world."
248                 raise SystemExit(msg)
249             obey_lines_in_file(io_db["path_worldconf"], "world config ",
250                                do_record=True)
251             obey("MAKE_WORLD " + str(int(time.time())), "in file",
252                  do_record=True)
253         while True:
254             obey(read_command(), "in file", do_record=True)
255 except SystemExit as exit:
256     print("ABORTING: " + exit.args[0])
257 except:
258     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
259     raise
260 finally:
261     cleanup_server_io()
262     # print("DUMMY: (Clean up C heap.)")