Permalink
Browse files

First implementation of multi-terminal recorder functionality.

  • Loading branch information...
1 parent b8854a5 commit 3370be35ac37aac6b43d0c13638d3199a2f17f02 @rfk committed Aug 7, 2012
Showing with 578 additions and 166 deletions.
  1. +1 −13 .gitignore
  2. +5 −0 TODO.txt
  3. +50 −153 playitagainsam/__init__.py
  4. +208 −0 playitagainsam/player.py
  5. +220 −0 playitagainsam/recorder.py
  6. +94 −0 playitagainsam/util.py
View
@@ -1,6 +1,5 @@
*.py[co]
-
-# Packages
+*.swp
*.egg
*.egg-info
dist
@@ -10,18 +9,7 @@ parts
bin
var
sdist
-develop-eggs
.installed.cfg
-
-# Installer logs
pip-log.txt
-
-# Unit test / coverage reports
.coverage
.tox
-
-#Translations
-*.mo
-
-#Mr Developer
-.mr.developer.cfg
View
@@ -0,0 +1,5 @@
+
+ * editing out ECHO of backspace
+ * child process watching, cleanup etc.
+ * robustness and error handling.
+
View
@@ -36,11 +36,12 @@
Sessions are recorded as a JSON file. The outer JSON object contains metadata
along with an "events" member. Each event is one of the following types:
- { type: "BEGIN", term: <uuid> }
- { type: "READ", term: <uuid>, data: <data> }
- { type: "WRITE", term: <uuid>, data: <data> }
- { type: "ECHO", term: <uuid>, data: <data> }
- { type: "END", term: <uuid> }
+ { act: "OPEN", term: <uuid> }
+ { act: "READ", term: <uuid>, data: <data> }
+ { act: "WRITE", term: <uuid>, data: <data> }
+ { act: "ECHO", term: <uuid>, data: <data> }
+ { act: "PAUSE", duration: <duration> }
+ { act: "CLOSE", term: <uuid> }
{
events: [
@@ -58,151 +59,47 @@
import os
-import sys
-import tty
-import pty
-import termios
-import select
-import optparse
-import time
-
-from subprocess import MAXFD
-
-
-class no_echo(object):
- """Context-manager that blocks echoing of keys typed in tty."""
-
- def __init__(self, fd=None):
- if fd is None:
- fd = sys.stdin.fileno()
- elif hasattr(fd, "fileno"):
- fd = fd.fileno()
- self.fd = fd
-
- def __enter__(self):
- self.old_attr = termios.tcgetattr(self.fd)
- new_attr = list(self.old_attr)
- new_attr[3] = new_attr[3] & ~termios.ECHO
- termios.tcsetattr(self.fd, termios.TCSADRAIN, new_attr)
- tty.setraw(sys.stdin)
-
- def __exit__(self, exc_typ, exc_val, exc_tb):
- termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old_attr)
-
-
-def get_fd(file_or_fd, default=None):
- fd = file_or_fd
- if fd is None:
- fd = default
- if hasattr(fd, "fileno"):
- fd = fd.fileno()
- return fd
-
-
-def record_session(logfile, argv=None, stdin=None, stdout=None):
- # Find the program to execute. Use the default shell by default.
- if argv is None:
- argv = os.environ.get("SHELL", "/bin/sh")
- if isinstance(argv, basestring):
- argv = [argv]
- # Grab file descriptors for stdin and stdout, we're going to
- # to lots of low-level IO on them.
- stdin_fd = get_fd(stdin, default=sys.stdin)
- stdout_fd = get_fd(stdout, default=sys.stdout)
- # Fork the child with a pty.
- child_pid, child_fd = pty.fork()
- if child_pid == 0:
- os.closerange(3, MAXFD)
- os.execv(argv[0], argv)
- def wait_for_activity():
- ready, _, _ = select.select([child_fd, stdin_fd], [], [])
- return ready
- def read_output():
- output = []
- try:
- ready, _, _ = select.select([child_fd, stdin_fd], [], [])
- while child_fd in ready:
- c = os.read(child_fd, 1)
- if not c:
- break
- output.append(c)
- os.write(stdout_fd, c)
- ready, _, _ = select.select([child_fd, stdin_fd], [], [], 0)
- finally:
- if output:
- logfile.write("W %s\n" % ("".join(output).encode("string-escape"),))
- def read_keypress():
- c = ""
- ready, _, _ = select.select([child_fd, stdin_fd], [], [])
- if stdin_fd in ready:
- c = os.read(stdin_fd, 1)
- if c:
- logfile.write("R %s\n" % (c.encode("string-escape"),))
- os.write(child_fd, c)
- return c
- # Shuffle data back and forth between our terminal and the pty.
- # Log everything.
- with no_echo(stdin_fd):
- try:
- while True:
- ts1 = time.time()
- ready = wait_for_activity()
- ts2 = time.time()
- if stdin_fd in ready:
- read_keypress()
- read_output()
- else:
- logfile.write("P %.6f\n" % (ts2 - ts1,))
- read_output()
- except EnvironmentError:
- pass
-
-
-def replay_session(logfile, stdin=None, stdout=None):
- # Grab file descriptors for stdin and stdout, we're going to
- # to lots of low-level IO on them.
- stdin_fd = get_fd(stdin, default=sys.stdin)
- stdout_fd = get_fd(stdout, default=sys.stdout)
- # Replay the session, controlling timing from keyboard.
- with no_echo(stdin):
- try:
- while True:
- ln = logfile.readline()
- if not ln:
- break
- act = ln[0]
- data = ln[2:-1].decode("string-escape")
- if act == "P":
- time.sleep(float(data))
- elif act == "W":
- os.write(stdout_fd, data)
- elif act == "R":
- c = os.read(stdin_fd, 1)
- if data in ("\n", "\r"):
- while c not in ("\n", "\r"):
- c = os.read(stdin_fd, 1)
- c = os.read(stdin_fd, 1)
- while c not in ("\n", "\r"):
- c = os.read(stdin_fd, 1)
- except EnvironmentError:
- pass
-
-
-if __name__ == "__main__":
-
- parser = optparse.OptionParser()
- parser.add_option("-f", "--logfile", default="session.log",
- help="file in which to store the session log",)
- parser.add_option("-c", "--command",
- help="command to execute (by default, your shell)")
-
- opts, args = parser.parse_args(sys.argv)
-
- if args[1] == "record":
- with open(opts.logfile, "w") as logfile:
- record_session(logfile, opts.command)
- elif args[1] == "replay":
- with open(opts.logfile, "r") as logfile:
- replay_session(logfile)
- else:
- raise ValueError("unknown command %r" % (args[1],))
+import json
+import argparse
+import threading
+
+import playitagainsam.util
+import playitagainsam.recorder
+
+
+def main(argv):
+ parser = argparse.ArgumentParser()
+ 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",
+ 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")
+
+ 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":
+ raise NotImplementedError
Oops, something went wrong.

0 comments on commit 3370be3

Please sign in to comment.