Class and module refactor

Conflicts:
	bin/asciiio
This commit is contained in:
Marcin Kulik
2012-11-04 22:52:49 +01:00
committed by Marcin Kulik
parent 477d36ea57
commit 57c55d3c16
12 changed files with 394 additions and 236 deletions

View File

@@ -5,7 +5,7 @@ bin/asciiio: tmp/asciiio.zip
cat tmp/asciiio.zip >> bin/asciiio
chmod +x bin/asciiio
tmp/asciiio.zip: src/__main__.py src/constants.py src/asciicast.py src/pty_recorder.py src/timed_file.py src/uploader.py src/config.py src/options.py src/help_text.py src/cli.py
tmp/asciiio.zip: src/__main__.py src/asciicasts.py src/recorders.py src/timed_file.py src/uploader.py src/config.py src/options.py src/cli.py src/asciicast_recorder.py
mkdir -p tmp
rm -rf tmp/asciiio.zip
cd src && zip ../tmp/asciiio.zip *.py

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env python
from cli import CLI
import cli
def main():
CLI().run()
cli.run()
if __name__ == '__main__':
main()

View File

@@ -1,97 +0,0 @@
import os
import sys
import time
import subprocess
import json
import shutil
from constants import BASE_DIR
from pty_recorder import PtyRecorder
from uploader import Uploader
class AsciiCast(object):
QUEUE_DIR = BASE_DIR + "/queue"
def __init__(self, config, options):
self.config = config
self.api_url = config.api_url()
self.user_token = config.user_token()
self.path = AsciiCast.QUEUE_DIR + "/%i" % int(time.time())
self.command = options.command
self.title = options.title
self.record_input = options.record_input
self.always_yes = options.always_yes
self.duration = None
def create(self):
self._record()
if self.confirm_upload():
return self._upload()
else:
self._delete()
def confirm_upload(self):
if self.always_yes:
return True
sys.stdout.write("~ Do you want to upload it? [Y/n] ")
answer = sys.stdin.readline().strip()
return answer == 'y' or answer == 'Y' or answer == ''
def _record(self):
os.makedirs(self.path)
self.recording_start = time.time()
command = self.command or os.environ['SHELL'].split()
PtyRecorder(self.path, command, self.record_input).run()
self.duration = time.time() - self.recording_start
self._save_metadata()
def _save_metadata(self):
info_file = open(self.path + '/meta.json', 'wb')
# RFC 2822
recorded_at = time.strftime("%a, %d %b %Y %H:%M:%S +0000",
time.gmtime(self.recording_start))
command = self.command and ' '.join(self.command)
uname = self._get_cmd_output(['uname', '-srvp'])
username = os.environ['USER']
shell = os.environ['SHELL']
term = os.environ['TERM']
lines = int(self._get_cmd_output(['tput', 'lines']))
columns = int(self._get_cmd_output(['tput', 'cols']))
data = {
'username' : username,
'user_token' : self.user_token,
'duration' : self.duration,
'recorded_at': recorded_at,
'title' : self.title,
'command' : command,
'shell' : shell,
'uname' : uname,
'term' : {
'type' : term,
'lines' : lines,
'columns': columns
}
}
json_string = json.dumps(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.config, self.path).upload()
if url:
print url
return True
else:
return False
def _delete(self):
shutil.rmtree(self.path)

95
src/asciicast_recorder.py Normal file
View File

@@ -0,0 +1,95 @@
import os
import time
import subprocess
from asciicasts import Asciicast
from recorders import ProcessRecorder
# def record(command, queue_dir_path):
# # id = int(time.time())
# # path = "%s/%i" % (queue_dir_path, id)
# # asciicast = Asciicast(path)
# if sys.stdin.isatty():
# record_process(command, asciicast.stdout_file)
# else:
# record_stdin(asciicast.stdout_file)
# def record_stdin(stdout_file, stdin=sys.stdin):
# def record_process(command, stdout_file, stdin_file=None):
def record(queue_dir_path, user_token, options):
# id = int(time.time())
# path = "%s/%i" % (queue_dir_path, id)
# asciicast = Asciicast(path)
self.command = options.command
self.record_input = options.record_input
self.always_yes = options.always_yes
self.recording_start_time = None
self.duration = None
self.record_command()
self.save_metadata(user_token=user_token, title=options.title)
return asciicast
def record_command(self):
self.recording_start_time = time.time()
cmd = self.command or os.environ['SHELL'].split()
stdin_file = None
if self.record_input:
stdin_file = self.asciicast.stdin_file
self.asciicast.open_files()
recorder = ProcessRecorder(cmd, self.asciicast.stdout_file, stdin_file)
recorder.run()
now = time.time()
self.duration = now - self.recording_start_time
self.asciicast.close_files()
def save_metadata(self):
# RFC 2822
recorded_at = time.strftime("%a, %d %b %Y %H:%M:%S +0000",
time.gmtime(self.recording_start_time))
command = self.command and ' '.join(self.command)
uname = get_command_output(['uname', '-srvp'])
username = os.environ['USER']
shell = os.environ['SHELL']
term = os.environ['TERM']
lines = int(get_command_output(['tput', 'lines']))
columns = int(get_command_output(['tput', 'cols']))
data = {
'username' : username,
'user_token' : self.user_token,
'duration' : self.duration,
'recorded_at': recorded_at,
'title' : self.title,
'command' : command,
'shell' : shell,
'uname' : uname,
'term' : {
'type' : term,
'lines' : lines,
'columns': columns
}
}
self.asciicast.save_metadata(data)
def get_command_output(args):
process = subprocess.Popen(args, stdout=subprocess.PIPE)
return process.communicate()[0].strip()

59
src/asciicasts.py Normal file
View File

@@ -0,0 +1,59 @@
import os
import glob
import shutil
import json
from timed_file import TimedFile
def pending(dir):
filenames = glob.glob(dir + '/*/*.time')
return [Asciicast(os.path.dirname(f)) for f in filenames]
class Asciicast(object):
def __init__(self, dir_path):
self.dir_path = dir_path
if not os.path.isdir(self.dir_path):
os.makedirs(self.dir_path)
self.stdout_file = TimedFile(self.dir_path + '/stdout')
self.stdin_file = TimedFile(self.dir_path + '/stdin')
@property
def metadata_filename(self):
return self.dir_path + '/meta.json'
@property
def stdout_data_filename(self):
return self.stdout_file.data_filename
@property
def stdout_timing_filename(self):
return self.stdout_file.timing_filename
@property
def stdin_data_filename(self):
return self.stdin_file.data_filename
@property
def stdin_timing_filename(self):
return self.stdin_file.timing_filename
def open_files(self):
self.stdout_file.start_timing()
self.stdin_file.start_timing()
def close_files(self):
self.stdout_file.close()
self.stdin_file.close()
def save_metadata(self, data):
json_string = json.dumps(data, sort_keys=True, indent=4)
with open(self.metadata_filename, 'wb') as f:
f.write(json_string + '\n')
def destroy(self):
shutil.rmtree(self.dir_path)

View File

@@ -1,66 +1,144 @@
import os
import sys
import glob
import help_text
import asciicasts
from config import Config
from options import Options
from constants import SCRIPT_NAME
from asciicast import AsciiCast
from uploader import Uploader
import asciicast_recorder
class CLI:
'''Parses command-line options and xxxxxxxxxxxxxxxxx.'''
def run(self):
self.config = Config()
self.options = Options(sys.argv)
SCRIPT_NAME = os.path.basename(sys.argv[0])
action = self.options.action
config = Config()
options = Options(sys.argv)
if action == 'rec':
self.check_pending()
self.record()
elif action == 'upload':
self.upload_pending()
elif action == 'auth':
self.auth()
elif action == 'help':
self.help()
elif action == 'version':
self.version()
else:
print('Unknown action: %s' % action)
print('Run "%s --help" for list of available options' % SCRIPT_NAME)
def record(self):
if not AsciiCast(self.config, self.options).create():
sys.exit(1)
def run():
action = options.action
def auth(self):
url = '%s/connect/%s' % (self.config.api_url(), self.config.user_token())
print 'Open following URL in your browser to authenticate and/or claim ' \
'recorded asciicasts:\n\n%s' % url
if action == 'rec':
record()
elif action == 'upload':
upload_all_pending()
elif action == 'auth':
authenticate()
elif action == 'help':
print_help()
elif action == 'version':
print_version()
else:
handle_unknown_action(action)
def check_pending(self):
num = len(self.pending_list())
if num > 0:
print 'Warning: %i recorded asciicasts weren\'t uploaded. ' \
'Run "%s upload" to upload them or delete them with "rm -rf %s/*".' \
% (num, SCRIPT_NAME, AsciiCast.QUEUE_DIR)
def upload_pending(self):
print 'Uploading pending asciicasts...'
for path in self.pending_list():
url = Uploader(self.config, path).upload()
if url:
print url
# Actions
def pending_list(self):
return [os.path.dirname(p) for p in glob.glob(AsciiCast.QUEUE_DIR + '/*/*.time')]
def record():
# check_pending()
def version(self):
print 'asciiio 1.0.1'
# id = int(time.time())
# path = "%s/%i" % (queue_dir_path, id)
# asciicast = Asciicast(path)
def help(self):
print help_text.TEXT
asciicast = Asciicast()
asciicast.command = options.command
asciicast.title = options.title
start_time = time.time()
if sys.stdin.isatty():
record_process(command, asciicast.stdout_file)
else:
record_stdin(asciicast.stdout_file)
end_time = time.time()
asciicast.recorded_at = start_time
asciicast.duration = end_time - start_time
asciicast.save()
# asciicast = asciicast_recorder.record(
# config.queue_dir_path, config.user_token, options
# )
# asciicast = recorder.record()
if is_upload_requested():
print '~ Uploading...'
upload_asciicast(asciicast)
def upload_all_pending():
print 'Uploading pending asciicasts...'
for asciicast in pending_asciicasts():
upload_asciicast(asciicast)
def authenticate():
url = '%s/connect/%s' % (config.api_url, config.user_token)
print 'Open following URL in your browser to authenticate and/or ' \
'claim recorded asciicasts:\n\n%s' % url
def print_help():
print HELP_TEXT
def print_version():
print 'asciiio 1.0.1'
def handle_unknown_action(action):
print('Unknown action: %s' % action)
print('Run "%s --help" for list of available options' % SCRIPT_NAME)
sys.exit(1)
# Helpers
def check_pending():
num = len(pending_asciicasts())
if num > 0:
print "Warning: %i recorded asciicasts weren't uploaded. " \
'Run "%s upload" to upload them or delete them with ' \
'"rm -rf %s/*".' \
% (num, SCRIPT_NAME, config.queue_dir_path)
def pending_asciicasts():
return asciicasts.pending(config.queue_dir_path)
def upload_asciicast(asciicast):
uploader = Uploader(config.api_url)
url = uploader.upload(asciicast)
if url:
print url
asciicast.destroy()
def is_upload_requested():
if options.always_yes:
return True
sys.stdout.write("~ Do you want to upload it? [Y/n] ")
answer = sys.stdin.readline().strip()
return answer == 'y' or answer == 'Y' or answer == ''
HELP_TEXT = '''usage: %s [-h] [-i] [-y] [-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
auth authenticate and/or claim recorded asciicasts
Optional arguments:
-c command run specified command instead of shell ($SHELL)
-t title specify title of recorded asciicast
-y don't prompt for confirmation
-h, --help show this help message and exit
--version show version information''' % SCRIPT_NAME

View File

@@ -6,7 +6,7 @@ class Config:
def __init__(self):
self.base_dir_path = os.path.expanduser("~/.ascii.io")
self.config_file_path = '%s/config' % self.base_dir_path
self.config_filename = '%s/config' % self.base_dir_path
self.queue_dir_path = '%s/queue' % self.base_dir_path
self.create_base_dir()
@@ -23,14 +23,15 @@ class Config:
config.add_section('record')
try:
config.read(self.config_file_path)
config.read(self.config_filename)
except ConfigParser.ParsingError:
print('Config file %s contains syntax errors' %
self.config_file_path)
self.config_filename)
sys.exit(2)
self.config = config
@property
def api_url(self):
try:
api_url = self.config.get('api', 'url')
@@ -41,6 +42,7 @@ class Config:
return api_url
@property
def user_token(self):
try:
user_token = self.config.get('user', 'token')
@@ -48,7 +50,7 @@ class Config:
user_token = str(uuid.uuid1())
self.config.set('user', 'token', user_token)
with open(self.config_file_path, 'wb') as configfile:
self.config.write(configfile)
with open(self.config_filename, 'wb') as f:
self.config.write(f)
return user_token

View File

@@ -1,4 +0,0 @@
import os
import sys
SCRIPT_NAME = os.path.basename(sys.argv[0])

View File

@@ -1,17 +0,0 @@
from constants import SCRIPT_NAME
TEXT = '''usage: %s [-h] [-i] [-y] [-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
auth authenticate and/or claim recorded asciicasts
Optional arguments:
-c command run specified command instead of shell ($SHELL)
-t title specify title of recorded asciicast
-y don't prompt for confirmation
-h, --help show this help message and exit
--version show version information''' % SCRIPT_NAME

View File

@@ -1,3 +1,5 @@
# TODO REFA
import os
import pty
import subprocess
@@ -8,44 +10,70 @@ import fcntl
import termios
import select
from timed_file import TimedFile
class PtyRecorder(object):
import time
import select
class StdinRecorder(object):
def __init__(self, stdout_file):
self.stdout_file = stdout_file
def run(self):
while 1:
line = sys.stdin.readline()
if len(line) == 0:
break
self.stdout_file.write(data)
# descriptor = 0
# while 1:
# try:
# rfds, wfds, xfds = select.select([descriptor], [], [])
# except select.error, e:
# if e[0] == 4: # Interrupted system call.
# continue
# if descriptor in rfds:
# data = os.read(descriptor, 1024)
# if len(data) == 0:
# break
# # self._write_stdout(data)
# self.stdout_file.write(data)
# # print time.time()
# # print len(data)
class ProcessRecorder(object):
'''Pseudo-terminal recorder.
Creates new pseudo-terminal for spawned process
and saves stdin/stderr (and timing) to files.
'''
def __init__(self, path, command, record_input):
self.master_fd = None
self.path = path
def __init__(self, command, stdout_file, stdin_file=None):
self.command = command
self.record_input = record_input
self.stdout_file = stdout_file
self.stdin_file = stdin_file
self.master_fd = None
def run(self):
self._open_files()
self.reset_terminal()
self._write_stdout('~ Asciicast recording started. Hit ^D (that\'s Ctrl+D) or type "exit" to finish.\n\n')
success = self._spawn()
self.reset_terminal()
self._write_stdout('~ Asciicast recording finished.\n')
self._close_files()
return success
def reset_terminal(self):
subprocess.call(["reset"])
def _open_files(self):
self.stdout_file = TimedFile(self.path + '/stdout')
if self.record_input:
self.stdin_file = TimedFile(self.path + '/stdin')
def _close_files(self):
self.stdout_file.close()
if self.record_input:
self.stdin_file.close()
def _spawn(self):
'''Create a spawned process.
@@ -138,7 +166,7 @@ class PtyRecorder(object):
'''Handles new data on child process stdin.'''
self._write_master(data)
if self.record_input:
if self.stdin_file:
self.stdin_file.write(data)
def _write_stdout(self, data):

View File

@@ -2,34 +2,44 @@ import time
import StringIO
import bz2
class TimedFile(object):
'''File wrapper that records write times in separate file.'''
def __init__(self, filename):
self.filename = filename
self.data_filename = filename
self.timing_filename = filename + '.time'
self.data_file = StringIO.StringIO()
self.time_file = StringIO.StringIO()
self.mem_data_file = None
self.mem_timing_file = None
self.old_time = time.time()
self.start_timing()
def start_timing(self):
self.prev_time = time.time()
def write(self, data):
self.data_file.write(data)
if not self.mem_data_file:
self.mem_data_file = StringIO.StringIO()
self.mem_timing_file = StringIO.StringIO()
now = time.time()
delta = now - self.old_time
self.time_file.write("%f %d\n" % (delta, len(data)))
self.old_time = now
delta = now - self.prev_time
self.prev_time = now
self.mem_data_file.write(data)
self.mem_timing_file.write("%f %d\n" % (delta, len(data)))
def close(self):
mode = 'w'
if not self.mem_data_file:
return
bz2_data_file = bz2.BZ2File(self.filename, mode)
bz2_data_file.write(self.data_file.getvalue())
bz2_data_file = bz2.BZ2File(self.data_filename, 'w')
bz2_data_file.write(self.mem_data_file.getvalue())
bz2_data_file.close()
self.mem_data_file.close()
bz2_time_file = bz2.BZ2File(self.filename + '.time', mode)
bz2_time_file.write(self.time_file.getvalue())
bz2_time_file.close()
self.data_file.close()
self.time_file.close()
bz2_timing_file = bz2.BZ2File(self.timing_filename, 'w')
bz2_timing_file.write(self.mem_timing_file.getvalue())
bz2_timing_file.close()
self.mem_timing_file.close()

View File

@@ -1,6 +1,22 @@
import os
import subprocess
import shutil
class CurlFormData(object):
def __init__(self, namespace=None):
self.namespace = namespace
self.files = {}
def add_file(self, name, filename):
if self.namespace:
name = '%s[%s]' % (self.namespace, name)
self.files[name] = '@' + filename
def form_file_args(self):
return ' '.join(['-F %s="%s"' % (k, v) for k, v in self.files.iteritems()])
class Uploader(object):
'''Asciicast uploader.
@@ -8,26 +24,22 @@ class Uploader(object):
Uploads recorded script to website using HTTP based API.
'''
def __init__(self, config, path):
self.api_url = config.api_url()
self.path = path
def __init__(self, api_url):
self.upload_url = '%s/api/asciicasts' % api_url
def upload(self):
print '~ Uploading...'
def upload(self, asciicast):
form_data = CurlFormData('asciicast')
files = {
'meta': 'meta.json',
'stdout': 'stdout',
'stdout_timing': 'stdout.time'
}
form_data.add_file('meta', asciicast.metadata_filename)
if os.path.exists(self.path + '/stdin'):
files['stdin'] = 'stdin'
files['stdin_timing'] = 'stdin.time'
form_data.add_file('stdout', asciicast.stdout_data_filename)
form_data.add_file('stdout_timing', asciicast.stdout_timing_filename)
fields = ["-F asciicast[%s]=@%s/%s" % (f, self.path, files[f]) for f in files]
if os.path.exists(asciicast.stdin_data_filename):
form_data.add_file('stdin', asciicast.stdin_data_filename)
form_data.add_file('stdin_timing', asciicast.stdin_timing_filename)
cmd = "curl -sSf -o - %s %s" % (' '.join(fields), '%s/api/asciicasts' % self.api_url)
cmd = "curl -sSf -o - %s %s" % (form_data.form_file_args(), self.upload_url)
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
@@ -37,13 +49,5 @@ class Uploader(object):
# print >> sys.stderr, stderr
# sys.stderr.write(stderr)
os.write(2, stderr)
else:
self._remove_files()
if stdout:
return stdout
else:
return None
def _remove_files(self):
shutil.rmtree(self.path)
return stdout