mirror of
https://github.com/asciinema/asciinema.git
synced 2025-12-16 11:48:13 +01:00
Refactoring and cleanup
This commit is contained in:
276
bin/rec.py
276
bin/rec.py
@@ -15,6 +15,58 @@ import getopt
|
|||||||
import subprocess
|
import subprocess
|
||||||
import httplib, urllib
|
import httplib, urllib
|
||||||
import socket
|
import socket
|
||||||
|
import glob
|
||||||
|
|
||||||
|
SCRIPT_NAME = os.path.basename(sys.argv[0])
|
||||||
|
|
||||||
|
|
||||||
|
class AsciiCast(object):
|
||||||
|
BASE_DIR = os.path.expanduser("~/.ascii.io")
|
||||||
|
QUEUE_DIR = BASE_DIR + "/queue"
|
||||||
|
|
||||||
|
def __init__(self, command, title, record_input):
|
||||||
|
self.path = AsciiCast.QUEUE_DIR + "/%i" % int(time.time())
|
||||||
|
self.command = command
|
||||||
|
self.title = title
|
||||||
|
self.record_input = record_input
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
self._record()
|
||||||
|
self._upload()
|
||||||
|
|
||||||
|
def _record(self):
|
||||||
|
os.makedirs(self.path)
|
||||||
|
self._save_metadata()
|
||||||
|
PtyRecorder(self.path, self.command, self.record_input).run()
|
||||||
|
|
||||||
|
def _save_metadata(self):
|
||||||
|
info_file = open(self.path + '/meta.json', 'wb')
|
||||||
|
|
||||||
|
json_data = {
|
||||||
|
'title': self.title,
|
||||||
|
'command': ' '.join(self.command),
|
||||||
|
'term': {
|
||||||
|
'type': os.environ['TERM'],
|
||||||
|
'lines': int(self._get_cmd_output(['tput', 'lines'])),
|
||||||
|
'columns': int(self._get_cmd_output(['tput', 'cols'])),
|
||||||
|
},
|
||||||
|
'shell': os.environ['SHELL'],
|
||||||
|
'uname': self._get_cmd_output(['uname', '-osrvp'])
|
||||||
|
}
|
||||||
|
|
||||||
|
json_string = json.dumps(json_data, sort_keys=True, indent=4)
|
||||||
|
info_file.write(json_string + '\n')
|
||||||
|
info_file.close()
|
||||||
|
|
||||||
|
def _get_cmd_output(self, args):
|
||||||
|
process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||||
|
return process.communicate()[0].strip()
|
||||||
|
|
||||||
|
def _upload(self):
|
||||||
|
url = Uploader(self.path).upload()
|
||||||
|
if url:
|
||||||
|
print url
|
||||||
|
|
||||||
|
|
||||||
class TimedFile(object):
|
class TimedFile(object):
|
||||||
'''File wrapper that records write times in separate file.'''
|
'''File wrapper that records write times in separate file.'''
|
||||||
@@ -25,10 +77,6 @@ class TimedFile(object):
|
|||||||
self.time_file = open(filename + '.time', mode)
|
self.time_file = open(filename + '.time', mode)
|
||||||
self.old_time = time.time()
|
self.old_time = time.time()
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.data_file.close()
|
|
||||||
self.time_file.close()
|
|
||||||
|
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
self.data_file.write(data)
|
self.data_file.write(data)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -36,6 +84,10 @@ class TimedFile(object):
|
|||||||
self.time_file.write("%f %d\n" % (delta, len(data)))
|
self.time_file.write("%f %d\n" % (delta, len(data)))
|
||||||
self.old_time = now
|
self.old_time = now
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.data_file.close()
|
||||||
|
self.time_file.close()
|
||||||
|
|
||||||
|
|
||||||
class PtyRecorder(object):
|
class PtyRecorder(object):
|
||||||
'''Pseudo-terminal recorder.
|
'''Pseudo-terminal recorder.
|
||||||
@@ -44,31 +96,31 @@ class PtyRecorder(object):
|
|||||||
and saves stdin/stderr (and timing) to files.
|
and saves stdin/stderr (and timing) to files.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, base_filename, command, record_input):
|
def __init__(self, path, command, record_input):
|
||||||
self.master_fd = None
|
self.master_fd = None
|
||||||
self.base_filename = base_filename
|
self.path = path
|
||||||
self.command = command
|
self.command = command
|
||||||
self.record_input = record_input
|
self.record_input = record_input
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.open_files()
|
self._open_files()
|
||||||
self.write_stdout('\n~ Asciicast recording started.\n')
|
self._write_stdout('\n~ Asciicast recording started.\n')
|
||||||
success = self.spawn()
|
success = self._spawn()
|
||||||
self.write_stdout('\n~ Asciicast recording finished.\n')
|
self._write_stdout('\n~ Asciicast recording finished.\n')
|
||||||
self.close_files()
|
self._close_files()
|
||||||
return success
|
return success
|
||||||
|
|
||||||
def open_files(self):
|
def _open_files(self):
|
||||||
self.stdout_file = TimedFile(self.base_filename + '.stdout')
|
self.stdout_file = TimedFile(self.path + '/stdout')
|
||||||
if self.record_input:
|
if self.record_input:
|
||||||
self.stdin_file = TimedFile(self.base_filename + '.stdin')
|
self.stdin_file = TimedFile(self.path + '/stdin')
|
||||||
|
|
||||||
def close_files(self):
|
def _close_files(self):
|
||||||
self.stdout_file.close()
|
self.stdout_file.close()
|
||||||
if self.record_input:
|
if self.record_input:
|
||||||
self.stdin_file.close()
|
self.stdin_file.close()
|
||||||
|
|
||||||
def spawn(self):
|
def _spawn(self):
|
||||||
'''Create a spawned process.
|
'''Create a spawned process.
|
||||||
|
|
||||||
Based on pty.spawn() from standard library.
|
Based on pty.spawn() from standard library.
|
||||||
@@ -76,8 +128,7 @@ class PtyRecorder(object):
|
|||||||
|
|
||||||
assert self.master_fd is None
|
assert self.master_fd is None
|
||||||
|
|
||||||
pid, master_fd = pty.fork()
|
pid, self.master_fd = pty.fork()
|
||||||
self.master_fd = master_fd
|
|
||||||
|
|
||||||
if pid == pty.CHILD:
|
if pid == pty.CHILD:
|
||||||
os.execlp(self.command[0], *self.command)
|
os.execlp(self.command[0], *self.command)
|
||||||
@@ -99,7 +150,7 @@ class PtyRecorder(object):
|
|||||||
if restore:
|
if restore:
|
||||||
tty.tcsetattr(pty.STDIN_FILENO, tty.TCSAFLUSH, mode)
|
tty.tcsetattr(pty.STDIN_FILENO, tty.TCSAFLUSH, mode)
|
||||||
|
|
||||||
os.close(master_fd)
|
os.close(self.master_fd)
|
||||||
self.master_fd = None
|
self.master_fd = None
|
||||||
signal.signal(signal.SIGWINCH, old_handler)
|
signal.signal(signal.SIGWINCH, old_handler)
|
||||||
|
|
||||||
@@ -126,123 +177,81 @@ class PtyRecorder(object):
|
|||||||
def _copy(self):
|
def _copy(self):
|
||||||
'''Main select loop.
|
'''Main select loop.
|
||||||
|
|
||||||
Passes control to self.master_read() or self.stdin_read()
|
Passes control to self._master_read() or self._stdin_read()
|
||||||
when new data arrives.
|
when new data arrives.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
assert self.master_fd is not None
|
assert self.master_fd is not None
|
||||||
master_fd = self.master_fd
|
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
try:
|
try:
|
||||||
rfds, wfds, xfds = select.select([master_fd, pty.STDIN_FILENO], [], [])
|
rfds, wfds, xfds = select.select([self.master_fd, pty.STDIN_FILENO], [], [])
|
||||||
except select.error, e:
|
except select.error, e:
|
||||||
if e[0] == 4: # Interrupted system call.
|
if e[0] == 4: # Interrupted system call.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if master_fd in rfds:
|
if self.master_fd in rfds:
|
||||||
data = os.read(self.master_fd, 1024)
|
data = os.read(self.master_fd, 1024)
|
||||||
self.handle_master_read(data)
|
self._handle_master_read(data)
|
||||||
|
|
||||||
if pty.STDIN_FILENO in rfds:
|
if pty.STDIN_FILENO in rfds:
|
||||||
data = os.read(pty.STDIN_FILENO, 1024)
|
data = os.read(pty.STDIN_FILENO, 1024)
|
||||||
self.handle_stdin_read(data)
|
self._handle_stdin_read(data)
|
||||||
|
|
||||||
def handle_master_read(self, data):
|
def _handle_master_read(self, data):
|
||||||
'''Handles new data on child process stdout.'''
|
'''Handles new data on child process stdout.'''
|
||||||
|
|
||||||
self.write_stdout(data)
|
self._write_stdout(data)
|
||||||
self.stdout_file.write(data)
|
self.stdout_file.write(data)
|
||||||
|
|
||||||
def handle_stdin_read(self, data):
|
def _handle_stdin_read(self, data):
|
||||||
'''Handles new data on child process stdin.'''
|
'''Handles new data on child process stdin.'''
|
||||||
|
|
||||||
self.write_master(data)
|
self._write_master(data)
|
||||||
if self.record_input:
|
if self.record_input:
|
||||||
self.stdin_file.write(data)
|
self.stdin_file.write(data)
|
||||||
|
|
||||||
def write_stdout(self, data):
|
def _write_stdout(self, data):
|
||||||
'''Writes to stdout as if the child process had written the data.'''
|
'''Writes to stdout as if the child process had written the data.'''
|
||||||
|
|
||||||
os.write(pty.STDOUT_FILENO, data)
|
os.write(pty.STDOUT_FILENO, data)
|
||||||
|
|
||||||
def write_master(self, data):
|
def _write_master(self, data):
|
||||||
'''Writes to the child process from its controlling terminal.'''
|
'''Writes to the child process from its controlling terminal.'''
|
||||||
|
|
||||||
master_fd = self.master_fd
|
assert self.master_fd is not None
|
||||||
assert master_fd is not None
|
|
||||||
while data != '':
|
while data != '':
|
||||||
n = os.write(master_fd, data)
|
n = os.write(self.master_fd, data)
|
||||||
data = data[n:]
|
data = data[n:]
|
||||||
|
|
||||||
|
|
||||||
class AsciiCast(object):
|
|
||||||
'''Asciicast model.
|
|
||||||
|
|
||||||
Manages recording and uploading of asciicast.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, command, title=None, record_input=False):
|
|
||||||
self.base_filename = str(int(time.time()))
|
|
||||||
self.command = command
|
|
||||||
self.title = title
|
|
||||||
self.record_input = record_input
|
|
||||||
|
|
||||||
def create(self):
|
|
||||||
ret = self.record()
|
|
||||||
if ret:
|
|
||||||
self.write_metadata()
|
|
||||||
self.upload()
|
|
||||||
|
|
||||||
def record(self):
|
|
||||||
rec = PtyRecorder(self.base_filename, self.command, self.record_input)
|
|
||||||
return rec.run()
|
|
||||||
|
|
||||||
def write_metadata(self):
|
|
||||||
info_file = open(self.base_filename + '.json', 'wb')
|
|
||||||
|
|
||||||
json_data = {
|
|
||||||
'title': self.title,
|
|
||||||
'command': ' '.join(self.command),
|
|
||||||
'term': {
|
|
||||||
'type': os.environ['TERM'],
|
|
||||||
'lines': int(self.get_output(['tput', 'lines'])),
|
|
||||||
'columns': int(self.get_output(['tput', 'cols'])),
|
|
||||||
},
|
|
||||||
'shell': os.environ['SHELL'],
|
|
||||||
'uname': self.get_output(['uname', '-osrvp'])
|
|
||||||
}
|
|
||||||
|
|
||||||
json_string = json.dumps(json_data, sort_keys=True, indent=2)
|
|
||||||
info_file.write(json_string + '\n')
|
|
||||||
info_file.close()
|
|
||||||
|
|
||||||
def get_output(self, args):
|
|
||||||
process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
|
||||||
return process.communicate()[0].strip()
|
|
||||||
|
|
||||||
def upload(self):
|
|
||||||
up = Uploader(self.base_filename)
|
|
||||||
up.upload()
|
|
||||||
|
|
||||||
|
|
||||||
class Uploader(object):
|
class Uploader(object):
|
||||||
'''Asciicast uploader.
|
'''Asciicast uploader.
|
||||||
|
|
||||||
Uploads recorded script to website using HTTP based API.
|
Uploads recorded script to website using HTTP based API.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, base_filename):
|
def __init__(self, path):
|
||||||
self.api_host = os.environ.get('TTV_API_HOST', 'localhost:3000')
|
self.api_host = os.environ.get('ASCIIIO_API_HOST', 'ascii.io')
|
||||||
self.api_path = '/scripts'
|
self.api_path = '/scripts'
|
||||||
self.base_filename = base_filename
|
self.path = path
|
||||||
|
|
||||||
def upload(self):
|
def upload(self):
|
||||||
params = self.build_params()
|
params = self._build_params()
|
||||||
self.make_request(params)
|
url = self._make_request(params)
|
||||||
|
|
||||||
def make_request(self, params):
|
return url
|
||||||
headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain"}
|
|
||||||
|
def _build_params(self):
|
||||||
|
params = urllib.urlencode({
|
||||||
|
'metadata': 'lolza'
|
||||||
|
})
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
def _make_request(self, params):
|
||||||
|
headers = { "Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain" }
|
||||||
conn = httplib.HTTPConnection(self.api_host)
|
conn = httplib.HTTPConnection(self.api_host)
|
||||||
try:
|
try:
|
||||||
conn.request("POST", self.api_path, params, headers)
|
conn.request("POST", self.api_path, params, headers)
|
||||||
@@ -253,29 +262,75 @@ class Uploader(object):
|
|||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
|
|
||||||
if response.status == 201:
|
if response.status == 201:
|
||||||
print response.read()
|
return response.read()
|
||||||
else:
|
else:
|
||||||
|
# TODO stderr
|
||||||
print 'Oops, something is not right. (%d: %s)' % (response.status,
|
print 'Oops, something is not right. (%d: %s)' % (response.status,
|
||||||
response.read())
|
response.read())
|
||||||
|
return None
|
||||||
|
|
||||||
def build_params(self):
|
|
||||||
params = urllib.urlencode({
|
|
||||||
'metadata': 'lolza'
|
|
||||||
})
|
|
||||||
|
|
||||||
return params
|
def check_pending():
|
||||||
|
num = len(pending_list())
|
||||||
|
if num > 0:
|
||||||
|
print 'Warning: %i recorded asciicasts weren\'t uploaded. ' \
|
||||||
|
'Run "%s -u" to upload them or delete them with "rm -rf %s/*".' \
|
||||||
|
% (num, SCRIPT_NAME, AsciiCast.QUEUE_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_pending():
|
||||||
|
print 'Uploading pending asciicasts...'
|
||||||
|
for path in pending_list():
|
||||||
|
url = Uploader(path).upload()
|
||||||
|
if url:
|
||||||
|
print url
|
||||||
|
|
||||||
|
|
||||||
|
def pending_list():
|
||||||
|
return glob.glob(AsciiCast.QUEUE_DIR + '/*/*.time')
|
||||||
|
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
text = '''usage: %s [-h] [-i] [-c <command>] [-t <title>] [action]
|
||||||
|
|
||||||
|
Asciicast recorder+uploader.
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
rec record asciicast (this is the default when no action given)
|
||||||
|
upload upload recorded (but not uploaded) asciicasts
|
||||||
|
|
||||||
|
Optional arguments:
|
||||||
|
-i record stdin (keystrokes will be shown during replay)
|
||||||
|
-c command run specified command instead of shell ($SHELL)
|
||||||
|
-t title specify title of recorded asciicast
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--version show version information''' % SCRIPT_NAME
|
||||||
|
print text
|
||||||
|
|
||||||
|
|
||||||
|
def print_version():
|
||||||
|
print 'ascii.io-clio v0.x'
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
'''Parses command-line options and creates asciicast.'''
|
'''Parses command-line options and creates asciicast.'''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
opts, args = getopt.getopt(sys.argv[1:], 'c:t:ih', ['help'])
|
opts, args = getopt.getopt(sys.argv[1:], 'c:t:ih', ['help', 'version'])
|
||||||
except getopt.error as msg:
|
except getopt.error as msg:
|
||||||
print('%s: %s' % (sys.argv[0], msg))
|
print('%s: %s' % (sys.argv[0], msg))
|
||||||
print('Run "%s --help" for list of available options' % sys.argv[0])
|
print('Run "%s --help" for list of available options' % sys.argv[0])
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
action = 'rec'
|
||||||
|
|
||||||
|
if len(args) > 1:
|
||||||
|
print('Too many arguments')
|
||||||
|
print('Run "%s --help" for list of available options' % sys.argv[0])
|
||||||
|
sys.exit(2)
|
||||||
|
elif len(args) == 1:
|
||||||
|
action = args[0]
|
||||||
|
|
||||||
command = os.environ['SHELL'].split()
|
command = os.environ['SHELL'].split()
|
||||||
title = None
|
title = None
|
||||||
record_input = False
|
record_input = False
|
||||||
@@ -284,6 +339,9 @@ def main():
|
|||||||
if opt in ('-h', '--help'):
|
if opt in ('-h', '--help'):
|
||||||
usage()
|
usage()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
elif opt == '--version':
|
||||||
|
print_version()
|
||||||
|
sys.exit(0)
|
||||||
elif opt == '-c':
|
elif opt == '-c':
|
||||||
command = arg.split()
|
command = arg.split()
|
||||||
elif opt == '-t':
|
elif opt == '-t':
|
||||||
@@ -291,21 +349,15 @@ def main():
|
|||||||
elif opt == '-i':
|
elif opt == '-i':
|
||||||
record_input = True
|
record_input = True
|
||||||
|
|
||||||
ac = AsciiCast(command, title, record_input)
|
if action == 'rec':
|
||||||
ac.create()
|
check_pending()
|
||||||
|
AsciiCast(command, title, record_input).create()
|
||||||
|
elif action == 'upload':
|
||||||
|
upload_pending()
|
||||||
|
else:
|
||||||
|
print('Unknown action: %s' % action)
|
||||||
|
print('Run "%s --help" for list of available options' % sys.argv[0])
|
||||||
|
|
||||||
|
|
||||||
def usage():
|
|
||||||
text = '''usage: %s [-h] [-i] [-c <command>] [-t <title>]
|
|
||||||
|
|
||||||
Asciicast recorder+uploader.
|
|
||||||
|
|
||||||
optional arguments:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
-i record stdin (keystrokes will be shown during replay)
|
|
||||||
-c command run specified command instead of shell ($SHELL)
|
|
||||||
-t title specify title of recorded asciicast''' % sys.argv[0]
|
|
||||||
print text
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user