Skip to content

Commit

Permalink
Refactor coordinator logic into base class
Browse files Browse the repository at this point in the history
  • Loading branch information
rfk committed Aug 8, 2012
1 parent 41c4d6f commit 9d725ab
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 137 deletions.
2 changes: 0 additions & 2 deletions TODO.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@

* factor out a common "controller" base class, along with proxy_to_controller
helper function
* editing out ECHO of backspace, so I can stuff up during recording
and have it magically fixed upon playback
* child process watching, cleanup etc.
Expand Down
64 changes: 39 additions & 25 deletions playitagainsam/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,40 +70,54 @@

def main(argv):
parser = argparse.ArgumentParser()
parser.add_argument("--join", action="store_true",
help="join an existing record/replay session")
subparsers = parser.add_subparsers(dest="subcommand")

# The "record" command.
parser_record = subparsers.add_parser("record")
parser_record.add_argument("datafile")
parser_record.add_argument("--shell",
help="the shell to execute",
default=os.environ.get("SHELL", "/bin/sh"))
parser_record.add_argument("--join")
# The "replay" command.
parser_replay = subparsers.add_parser("replay")
parser_replay.add_argument("datafile")
parser_replay.add_argument("--term")
parser_replay.add_argument("--terminal",
help="the terminal program to execute",
default="/usr/bin/gnome-terminal")

args = parser.parse_args(argv[1:])

if args.subcommand == "record":
if args.join is None:
events = playitagainsam.util.EventLog()
recorder = playitagainsam.recorder.Recorder(events)
t = threading.Thread(target=recorder.run)
t.setDaemon(True)
t.start()
addr = ("localhost", 12345)
else:
addr = args.join.split(":")
addr = (addr[0], int(addr[1]))
playitagainsam.recorder.spawn_in_recorder(addr, args.shell)
if args.join is None:
t.join()
with open(args.datafile, "w") as datafile:
data = {"events": events.events}
output = json.dumps(data, indent=2, sort_keys=True)
datafile.write(output)
elif args.subcommand == "replay":
with open(args.datafile, "r") as datafile:
events = json.loads(datafile.read())["events"]
player = playitagainsam.player.Replayer(events)
player.run()
sock_path = args.datafile + ".sock"
if os.path.exists(sock_path) and not args.join:
raise RuntimeError("session already in progress")

try:
if args.subcommand == "record":
recorder = None
if not args.join:
events = playitagainsam.util.EventLog()
recorder = playitagainsam.recorder.Recorder(events, sock_path)
recorder.start()
playitagainsam.recorder.spawn_in_recorder(sock_path, args.shell)
if recorder is not None:
recorder.join()
with open(args.datafile, "w") as datafile:
data = {"events": events.events}
output = json.dumps(data, indent=2, sort_keys=True)
datafile.write(output)

elif args.subcommand == "replay":
if not args.join:
with open(args.datafile, "r") as datafile:
events = json.loads(datafile.read())["events"]
player = playitagainsam.player.Player(events, args.terminal, sock_path)
player.start()
playitagainsam.player.proxy_to_player(sock_path)
if player is not None:
player.join()

finally:
if os.path.exists(sock_path) and not args.join:
os.unlink(sock_path)
111 changes: 111 additions & 0 deletions playitagainsam/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Copyright (c) 2012, Ryan Kelly.
# All rights reserved; available under the terms of the MIT License.
"""
playitagainsam.coordinator: object for coordinating simulated terminals
========================================================================
This module provides a base class that can be used to coordinate input/output
for one or more simulated terminals. Each terminal is associated with a
"view" process that handles input and output.
"""

import os
import sys
import time
import select
import socket
import threading

from playitagainsam.util import get_fd, no_echo


class StopCoordinator(Exception):
"""Exception raised to stop execution of the coordinator."""
pass


class SocketCoordinator(object):
"""Object for coordinating activity between views and data processes."""

def __init__(self, sock_path):
self.__running = False
self.__run_thread = None
self.__ping_pipe_r, self.__ping_pipe_w = os.pipe()
self.sock_path = sock_path
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.bind(sock_path)
self.sock.listen(1)

def __del__(self):
self.__cleanup_pipes()

def __cleanup_pipes(self, os=os):
if self.__ping_pipe_r is not None:
os.close(self.__ping_pipe_r)
self.__ping_pipe_r = None
if self.__ping_pipe_w is not None:
os.close(self.__ping_pipe_w)
self.__ping_pipe_w = None

def start(self):
assert self.__run_thread is None
self.__running = True
def runit():
try:
self.run()
except StopCoordinator:
pass
finally:
self.cleanup()
self.__run_thread = threading.Thread(target=runit)
self.__run_thread.start()

def stop(self):
assert self.__run_thread is not None
self.__running = False
os.write(self.__ping_pipe_w, "X")

def join(self):
self.__run_thread.join()

def run(self):
raise NotImplementedError

def cleanup(self):
pass

def wait_for_data(self, fds, timeout=None):
fds = [self.__ping_pipe_r] + list(fds)
try:
ready, _, _ = select.select(fds, [], fds, timeout)
if not self.__running:
raise StopCoordinator
return ready
except OSError:
return []


def proxy_to_coordinator(socket_path, header=None, stdin=None, stdout=None):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(socket_path)
try:
stdin_fd = get_fd(stdin, sys.stdin)
stdout_fd = get_fd(stdout, sys.stdout)
with no_echo(stdin_fd):
if header is not None:
sock.sendall(header)
while True:
ready, _, _ = select.select([stdin_fd, sock], [], [])
if stdin_fd in ready:
c = os.read(stdin_fd, 1)
if c:
sock.send(c)
if sock in ready:
c = sock.recv(1024)
if not c:
break
os.write(stdout_fd, c)
finally:
sock.close()
56 changes: 27 additions & 29 deletions playitagainsam/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,22 @@
import socket

from playitagainsam.util import no_echo, get_fd, forkexec
from playitagainsam.coordinator import SocketCoordinator, proxy_to_coordinator


class Replayer(object):
class Player(SocketCoordinator):

def __init__(self, events):
def __init__(self, events, terminal, sock_path):
self.events = list(events)
self._ping_pipe_r, self._ping_pipe_w = os.pipe()
self.running = False
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind(("localhost", 12345))
self.sock.listen(1)
self.terminal = terminal
super(Player, self).__init__(sock_path)
self.terminals = {}
self.view_fds = {}

def __del__(self):
self._cleanup_pipes()

def _cleanup_pipes(self, os=os):
if getattr(self, "_ping_pipe_r", None) is not None:
os.close(self._ping_pipe_r)
self._ping_pipe_r = None
if getattr(self, "_ping_pipe_w", None) is not None:
os.close(self._ping_pipe_w)
self._ping_pipe_w = None

def stop(self):
self.running = False
os.write(self._ping_pipe_w, "X")

def run(self):
self.running = True
event_stream = self._iter_events()
try:
while self.running:
while True:
event = event_stream.next()
if event["act"] == "OPEN":
self._do_open_terminal(event["term"])
Expand All @@ -61,8 +42,12 @@ def run(self):
self._do_write(event["term"], event["data"])
except StopIteration:
pass

def cleanup(self):
for term in self.terminals:
self._do_close_terminal(term)
view_sock, = self.terminals[term]
view_sock.close()
super(Player, self).cleanup()

def _iter_events(self):
for event in self.events:
Expand All @@ -77,12 +62,21 @@ def _iter_events(self):
yield event

def _do_open_terminal(self, term):
child_pid = forkexec("/usr/bin/gnome-terminal", "-x", "/bin/bash", "-c", sys.executable + " -c \"from playitagainsam.recorder import proxy_to_recorder_addr; proxy_to_recorder_addr(('localhost', 12345))\" ; sleep 10")
ready = self.wait_for_data([self.sock], 0.1)
if self.sock not in ready:
# XXX TODO: wait for a keypress from some existing terminal
# to trigger the appearance of the terminal.
join_cmd = list(sys.argv)
join_cmd.insert(1, "--join")
forkexec(self.terminal, "-x", *join_cmd)
view_sock, _ = self.sock.accept()
self.terminals[term] = (view_sock, child_pid)
self.terminals[term] = (view_sock,)

def _do_close_terminal(self, term):
view_sock, client_pid = self.terminals[term]
view_sock, = self.terminals[term]
c = view_sock.recv(1)
while c not in ("\n", "\r"):
c = view_sock.recv(1)
view_sock.close()

def _do_read(self, term, wanted):
Expand All @@ -95,3 +89,7 @@ def _do_read(self, term, wanted):
def _do_write(self, term, data):
view_sock = self.terminals[term][0]
view_sock.sendall(data)


def proxy_to_player(sock_path, **kwds):
return proxy_to_coordinator(sock_path, **kwds)
Loading

0 comments on commit 9d725ab

Please sign in to comment.