import fcntl
import os
import pty
import select
import struct
import subprocess
import sys
import termios
import time
import pyte
COLS, ROWS = 160, 45
BINARY = os.environ.get("QUINJET", os.path.expanduser("~/.cargo/bin/quinjet"))
class Session:
def __init__(self, repository):
master, slave = pty.openpty()
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", ROWS, COLS, 0, 0))
self.master = master
self.process = subprocess.Popen(
[BINARY],
cwd=repository,
stdin=slave,
stdout=slave,
stderr=slave,
env=dict(os.environ, TERM="xterm-256color"),
preexec_fn=os.setsid,
)
os.close(slave)
self.screen = pyte.Screen(COLS, ROWS)
self.stream = pyte.ByteStream(self.screen)
self.group = os.getpgid(self.process.pid)
self.pump(2.5)
def pump(self, seconds):
deadline = time.time() + seconds
while time.time() < deadline:
ready, _, _ = select.select([self.master], [], [], 0.02)
if not ready:
continue
try:
chunk = os.read(self.master, 1 << 20)
except OSError:
return
if not chunk:
return
self.stream.feed(chunk)
def body(self):
return "\n".join(self.screen.display)
def type(self, text, settle=0.06):
for character in text:
os.write(self.master, character.encode())
time.sleep(settle)
def gh_children(self):
listing = subprocess.run(
["ps", "-eo", "pgid,etimes,args"], capture_output=True, text=True
).stdout.splitlines()
running = []
for line in listing[1:]:
parts = line.split(None, 2)
if len(parts) < 3 or parts[0] != str(self.group):
continue
if not parts[2].startswith("gh "):
continue
running.append((int(parts[1]), parts[2]))
return running
def open_pull_request(self, number, focus="3"):
self.type(focus)
self.pump(1.0)
self.type("\x7f" * 8, settle=0.04)
self.type(str(number))
self.pump(0.5)
for _ in range(4):
os.write(self.master, b"\r")
self.pump(1.5)
if "Source" in self.body() or "State" in self.body():
return True
return False
def close(self):
os.write(self.master, b"q")
self.pump(0.5)
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.kill()
def command_open(repository, number):
session = Session(repository)
try:
if not session.open_pull_request(number):
print("the pane never opened")
return 1
opened = time.time()
for _ in range(60):
session.pump(0.25)
if "Loading the conversation" not in session.body():
print(f"conversation rendered {time.time() - opened:.1f}s after the pane opened")
print("served from cache" if "cached" in session.body() else "read live")
return 0
print("the conversation never rendered")
return 1
finally:
session.close()
def command_switch(repository, first, second):
session = Session(repository)
try:
if not session.open_pull_request(first):
print(f"PR {first} never opened")
return 1
for _ in range(30):
session.pump(1.0)
if any("/logs" in command for _, command in session.gh_children()):
break
else:
print("the log warm-up never started, so there is nothing to queue behind")
return 1
print(f"warm-up running, switching to {second}")
session.open_pull_request(second, focus="/")
started = time.time()
overlapped = False
for _ in range(120):
session.pump(1.0)
running = [command for _, command in session.gh_children()]
if any("/logs" in c for c in running) and any(str(second) in c for c in running):
overlapped = True
if "Loading the conversation" not in session.body() and time.time() - started > 3:
elapsed = time.time() - started
print(f"PR {second} answered in {elapsed:.1f}s")
print(
"the warm-up and the switch overlapped"
if overlapped
else "the switch waited for the warm-up to drain"
)
return 0 if overlapped else 1
print(f"PR {second} never answered")
return 1
finally:
session.close()
def main():
if len(sys.argv) < 2:
print(__doc__)
return 2
action = sys.argv[1]
if action == "open" and len(sys.argv) == 4:
return command_open(sys.argv[2], sys.argv[3])
if action == "switch" and len(sys.argv) == 5:
return command_switch(sys.argv[2], sys.argv[3], sys.argv[4])
print(__doc__)
return 2
if __name__ == "__main__":
sys.exit(main())