Skip to content

Commit

Permalink
Rewrite startup_checks
Browse files Browse the repository at this point in the history
  • Loading branch information
zach2good committed Jan 24, 2025
1 parent bd25348 commit 8e286b1
Showing 1 changed file with 120 additions and 42 deletions.
162 changes: 120 additions & 42 deletions tools/ci/startup_checks.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,144 @@
#!/usr/bin/python

import io
import platform
import subprocess
import time
import signal
import threading
from queue import Queue, Empty

TEN_MINUTES_IN_SECONDS = 600
CHECK_INTERVAL_SECONDS = 5


def kill_all(processes):
"""Send SIGTERM to all running processes."""
for proc in processes:
if proc.poll() is None: # still running
proc.send_signal(signal.SIGTERM)


def reader_thread(proc, output_queue):
"""
Reads lines from proc.stdout and puts them into the output_queue
along with a reference to the proc.
When the process ends (stdout is closed), push a (proc, None)
to indicate it's done.
"""
with proc.stdout:
for line in proc.stdout:
# 'line' already in string form since we use text=True
output_queue.put((proc, line))
# Signal that this proc has ended
output_queue.put((proc, None))


def main():
print("Running exe startup checks...({})".format(platform.system()))

p0 = subprocess.Popen(
["xi_connect", "--log", "connect-server.log"], stdout=subprocess.PIPE
)
p1 = subprocess.Popen(
["xi_search", "--log", "search-server.log"], stdout=subprocess.PIPE
)
p2 = subprocess.Popen(
["xi_map", "--log", "game-server.log", "--load_all"], stdout=subprocess.PIPE
)
p3 = subprocess.Popen(
["xi_world", "--log", "world-server.log"], stdout=subprocess.PIPE
)
# Start the processes
# Use text=True (or universal_newlines=True) so we get strings instead of bytes.
processes = [
subprocess.Popen(
["xi_connect", "--log", "connect-server.log"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
),
subprocess.Popen(
["xi_search", "--log", "search-server.log"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
),
subprocess.Popen(
["xi_map", "--log", "game-server.log", "--load_all"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
),
subprocess.Popen(
["xi_world", "--log", "world-server.log"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
),
]

# Keep track of which processes have reported "ready to work"
ready_status = {proc: False for proc in processes}

print("Sleeping for 5 minutes...")
# Create a queue to receive stdout lines from all processes
output_queue = Queue()

# Start a reading thread for each process
threads = []
for proc in processes:
t = threading.Thread(
target=reader_thread, args=(proc, output_queue), daemon=True
)
t.start()
threads.append(t)

print(
f"Polling process output every {CHECK_INTERVAL_SECONDS}s for up to {TEN_MINUTES_IN_SECONDS}s..."
)

time.sleep(TEN_MINUTES_IN_SECONDS)
start_time = time.time()

print("Checking logs and killing exes...")
while True:
# If we've hit the timeout (10 minutes), fail
if time.time() - start_time > TEN_MINUTES_IN_SECONDS:
print("Timed out waiting for all processes to become ready.")
kill_all(processes)
exit(-1)

has_seen_output = False
error = False
for proc in {p0, p1, p2, p3}:
made_it_to_ready = False
print(proc.args[0])
proc.send_signal(signal.SIGTERM)
for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):
print(line.replace("\n", ""))
has_seen_output = True
if (
"error" in line.lower()
or "warning" in line.lower()
or "crash" in line.lower()
):
print("^^^")
error = True
# Poll the queue for new lines
# We'll keep pulling until it's empty (non-blocking)
while True:
try:
proc, line = output_queue.get_nowait()
except Empty:
break # No more lines at the moment

if "ready to work" in line:
made_it_to_ready = True
# If line is None, that means this proc ended
if line is None:
# If the process ended but wasn't marked ready => error
if not ready_status[proc]:
print(
f"ERROR: {proc.args[0]} exited before it was 'ready to work'."
)
kill_all(processes)
exit(-1)
else:
# We have an actual line of output
line_str = line.strip()
print(f"[{proc.args[0]}] {line_str}")

if not made_it_to_ready:
print("ERROR: Did not make it to ready state!")
error = True
# Check for error or warning text
lower_line = line_str.lower()
if any(x in lower_line for x in ["error", "warning", "crash"]):
print("^^^ Found error or warning in output.")
kill_all(processes)
print("Killing all processes and exiting with error.")
exit(-1)

if not has_seen_output:
print("ERROR: Did not get any output!")
# Check for "ready to work"
if "ready to work" in lower_line:
print(f"==> {proc.args[0]} is ready!")
ready_status[proc] = True

if error or not has_seen_output:
exit(-1)
# Check if all processes are marked ready
if all(ready_status.values()):
print(
"All processes reached 'ready to work'! Killing them and exiting successfully."
)
kill_all(processes)
exit(0)

time.sleep(5)
# Sleep until next poll
time.sleep(CHECK_INTERVAL_SECONDS)

if __name__ == "__main__":
main()

0 comments on commit 8e286b1

Please sign in to comment.