Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fleshing out with some prototype code.

  • Loading branch information...
commit b8854a5e80d1a8958a95f56ad440a9b9b11d93ac 1 parent 2306606
Ryan Kelly authored
5 ChangeLog.txt
... ... @@ -0,0 +1,5 @@
  1 +
  2 +v0.1.0:
  3 +
  4 + * Initial release.
  5 +
19 LICENSE.txt
... ... @@ -0,0 +1,19 @@
  1 +Copyright (c) 2012 Ryan Kelly
  2 +
  3 +Permission is hereby granted, free of charge, to any person obtaining a copy
  4 +of this software and associated documentation files (the "Software"), to deal
  5 +in the Software without restriction, including without limitation the rights
  6 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7 +copies of the Software, and to permit persons to whom the Software is
  8 +furnished to do so, subject to the following conditions:
  9 +
  10 +The above copyright notice and this permission notice shall be included in
  11 +all copies or substantial portions of the Software.
  12 +
  13 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19 +THE SOFTWARE.
5 MANIFEST.in
... ... @@ -0,0 +1,5 @@
  1 +
  2 +include README.rst
  3 +include LICENSE.txt
  4 +include ChangeLog.txt
  5 +
4 README.md
Source Rendered
... ... @@ -1,4 +0,0 @@
1   -playitagainsam
2   -==============
3   -
4   -record and replay interactive terminal sessions
0  README.rst
Source Rendered
No changes.
208 playitagainsam/__init__.py
... ... @@ -0,0 +1,208 @@
  1 +# Copyright (c) 2012, Ryan Kelly.
  2 +# All rights reserved; available under the terms of the MIT License.
  3 +"""
  4 +
  5 +playitagainsam: record and replay interactive terminal sessions
  6 +================================================================
  7 +
  8 +Playitagainsam is a tool and a corresponding file format for recording
  9 +and replating interactive terminal sessions. It takes inspiration from
  10 +the unix commands "script" and "ttyrec" and the python tool "playerpiano".
  11 +
  12 +Useful features include:
  13 +
  14 + * ability to replay with fake typing
  15 + * ability to replay sessions in multiple terminals
  16 +
  17 +Run the software using either the included "pias" script, or using the
  18 +python module-running syntax of "python -m playitagainsam".
  19 +
  20 +Record a session:
  21 +
  22 + $ pias record
  23 +
  24 +Join an existing recording as a new terminal:
  25 +
  26 + $ pias record --join addr
  27 +
  28 +Replay a recorded session:
  29 +
  30 + $ pias replay
  31 +
  32 +
  33 +Session Log Format
  34 +------------------
  35 +
  36 +Sessions are recorded as a JSON file. The outer JSON object contains metadata
  37 +along with an "events" member. Each event is one of the following types:
  38 +
  39 + { type: "BEGIN", term: <uuid> }
  40 + { type: "READ", term: <uuid>, data: <data> }
  41 + { type: "WRITE", term: <uuid>, data: <data> }
  42 + { type: "ECHO", term: <uuid>, data: <data> }
  43 + { type: "END", term: <uuid> }
  44 +
  45 + {
  46 + events: [
  47 + ]
  48 + }
  49 +
  50 +"""
  51 +
  52 +__ver_major__ = 0
  53 +__ver_minor__ = 1
  54 +__ver_patch__ = 0
  55 +__ver_sub__ = ""
  56 +__ver_tuple__ = (__ver_major__,__ver_minor__,__ver_patch__,__ver_sub__)
  57 +__version__ = "%d.%d.%d%s" % __ver_tuple__
  58 +
  59 +
  60 +import os
  61 +import sys
  62 +import tty
  63 +import pty
  64 +import termios
  65 +import select
  66 +import optparse
  67 +import time
  68 +
  69 +from subprocess import MAXFD
  70 +
  71 +
  72 +class no_echo(object):
  73 + """Context-manager that blocks echoing of keys typed in tty."""
  74 +
  75 + def __init__(self, fd=None):
  76 + if fd is None:
  77 + fd = sys.stdin.fileno()
  78 + elif hasattr(fd, "fileno"):
  79 + fd = fd.fileno()
  80 + self.fd = fd
  81 +
  82 + def __enter__(self):
  83 + self.old_attr = termios.tcgetattr(self.fd)
  84 + new_attr = list(self.old_attr)
  85 + new_attr[3] = new_attr[3] & ~termios.ECHO
  86 + termios.tcsetattr(self.fd, termios.TCSADRAIN, new_attr)
  87 + tty.setraw(sys.stdin)
  88 +
  89 + def __exit__(self, exc_typ, exc_val, exc_tb):
  90 + termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old_attr)
  91 +
  92 +
  93 +def get_fd(file_or_fd, default=None):
  94 + fd = file_or_fd
  95 + if fd is None:
  96 + fd = default
  97 + if hasattr(fd, "fileno"):
  98 + fd = fd.fileno()
  99 + return fd
  100 +
  101 +
  102 +def record_session(logfile, argv=None, stdin=None, stdout=None):
  103 + # Find the program to execute. Use the default shell by default.
  104 + if argv is None:
  105 + argv = os.environ.get("SHELL", "/bin/sh")
  106 + if isinstance(argv, basestring):
  107 + argv = [argv]
  108 + # Grab file descriptors for stdin and stdout, we're going to
  109 + # to lots of low-level IO on them.
  110 + stdin_fd = get_fd(stdin, default=sys.stdin)
  111 + stdout_fd = get_fd(stdout, default=sys.stdout)
  112 + # Fork the child with a pty.
  113 + child_pid, child_fd = pty.fork()
  114 + if child_pid == 0:
  115 + os.closerange(3, MAXFD)
  116 + os.execv(argv[0], argv)
  117 + def wait_for_activity():
  118 + ready, _, _ = select.select([child_fd, stdin_fd], [], [])
  119 + return ready
  120 + def read_output():
  121 + output = []
  122 + try:
  123 + ready, _, _ = select.select([child_fd, stdin_fd], [], [])
  124 + while child_fd in ready:
  125 + c = os.read(child_fd, 1)
  126 + if not c:
  127 + break
  128 + output.append(c)
  129 + os.write(stdout_fd, c)
  130 + ready, _, _ = select.select([child_fd, stdin_fd], [], [], 0)
  131 + finally:
  132 + if output:
  133 + logfile.write("W %s\n" % ("".join(output).encode("string-escape"),))
  134 + def read_keypress():
  135 + c = ""
  136 + ready, _, _ = select.select([child_fd, stdin_fd], [], [])
  137 + if stdin_fd in ready:
  138 + c = os.read(stdin_fd, 1)
  139 + if c:
  140 + logfile.write("R %s\n" % (c.encode("string-escape"),))
  141 + os.write(child_fd, c)
  142 + return c
  143 + # Shuffle data back and forth between our terminal and the pty.
  144 + # Log everything.
  145 + with no_echo(stdin_fd):
  146 + try:
  147 + while True:
  148 + ts1 = time.time()
  149 + ready = wait_for_activity()
  150 + ts2 = time.time()
  151 + if stdin_fd in ready:
  152 + read_keypress()
  153 + read_output()
  154 + else:
  155 + logfile.write("P %.6f\n" % (ts2 - ts1,))
  156 + read_output()
  157 + except EnvironmentError:
  158 + pass
  159 +
  160 +
  161 +def replay_session(logfile, stdin=None, stdout=None):
  162 + # Grab file descriptors for stdin and stdout, we're going to
  163 + # to lots of low-level IO on them.
  164 + stdin_fd = get_fd(stdin, default=sys.stdin)
  165 + stdout_fd = get_fd(stdout, default=sys.stdout)
  166 + # Replay the session, controlling timing from keyboard.
  167 + with no_echo(stdin):
  168 + try:
  169 + while True:
  170 + ln = logfile.readline()
  171 + if not ln:
  172 + break
  173 + act = ln[0]
  174 + data = ln[2:-1].decode("string-escape")
  175 + if act == "P":
  176 + time.sleep(float(data))
  177 + elif act == "W":
  178 + os.write(stdout_fd, data)
  179 + elif act == "R":
  180 + c = os.read(stdin_fd, 1)
  181 + if data in ("\n", "\r"):
  182 + while c not in ("\n", "\r"):
  183 + c = os.read(stdin_fd, 1)
  184 + c = os.read(stdin_fd, 1)
  185 + while c not in ("\n", "\r"):
  186 + c = os.read(stdin_fd, 1)
  187 + except EnvironmentError:
  188 + pass
  189 +
  190 +
  191 +if __name__ == "__main__":
  192 +
  193 + parser = optparse.OptionParser()
  194 + parser.add_option("-f", "--logfile", default="session.log",
  195 + help="file in which to store the session log",)
  196 + parser.add_option("-c", "--command",
  197 + help="command to execute (by default, your shell)")
  198 +
  199 + opts, args = parser.parse_args(sys.argv)
  200 +
  201 + if args[1] == "record":
  202 + with open(opts.logfile, "w") as logfile:
  203 + record_session(logfile, opts.command)
  204 + elif args[1] == "replay":
  205 + with open(opts.logfile, "r") as logfile:
  206 + replay_session(logfile)
  207 + else:
  208 + raise ValueError("unknown command %r" % (args[1],))
8 playitagainsam/__main__.py
... ... @@ -0,0 +1,8 @@
  1 +# Copyright (c) 2012, Ryan Kelly.
  2 +# All rights reserved; available under the terms of the MIT License.
  3 +
  4 +if __name__ == "__main__":
  5 + import sys
  6 + import playitagainsam
  7 + res = playitagainsam.main(sys.argv)
  8 + sys.exit(res)
7 scripts/pias
... ... @@ -0,0 +1,7 @@
  1 +#!/usr/bin/env python
  2 +
  3 +if __name__ == "__main__":
  4 + import sys
  5 + import playitagainsam
  6 + res = playitagainsam.main(sys.argv)
  7 + sys.exit(res)
BIN  setup.py
Binary file not shown

0 comments on commit b8854a5

Please sign in to comment.
Something went wrong with that request. Please try again.