shatty 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ gem "clamp"
4
+ gem "ftw"
5
+ gem "uuidtools"
data/Gemfile.lock ADDED
@@ -0,0 +1,27 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ addressable (2.2.6)
5
+ backports (2.3.0)
6
+ cabin (0.4.4)
7
+ json
8
+ clamp (0.3.1)
9
+ ftw (0.0.14)
10
+ addressable (= 2.2.6)
11
+ backports (= 2.3.0)
12
+ cabin (> 0)
13
+ http_parser.rb (= 0.5.3)
14
+ json (= 1.6.5)
15
+ minitest (> 0)
16
+ http_parser.rb (0.5.3)
17
+ json (1.6.5)
18
+ minitest (2.11.0)
19
+ uuidtools (2.1.3)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ clamp
26
+ ftw
27
+ uuidtools
data/Procfile ADDED
@@ -0,0 +1 @@
1
+ web: ruby web.rb
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # shatty
2
+
3
+ share tty.
4
+
5
+ ## Play a demo recording
6
+
7
+ ```
8
+ % ruby shatty.rb play examples/output.shatty
9
+ ```
10
+
11
+ ## Recording
12
+
13
+ ```
14
+ % shatty.rb record <command>
15
+ ```
16
+
17
+ By default will record to 'output.shatty'
18
+
19
+ ## Playback
20
+
21
+ ```
22
+ % shatty.rb play output.shatty
23
+ ```
24
+
25
+ ## Sharing
26
+
27
+ TBD.
28
+
29
+ * read-only
30
+ * read/write
31
+
32
+ ## Tricks
33
+
34
+ ### Record an active tmux session
35
+
36
+ ```bash
37
+ # From any shell in your tmux session:
38
+ % TMUX= ruby shatty.rb record --headless tmux -2 attach
39
+ ```
40
+
41
+ The '--headless' is required otherwise you end up tmux printing to tmux and you get a loop.
42
+
43
+
44
+ ## TODO
45
+
46
+ * Improved player
47
+ * Skip forward/back
48
+ * Tunable playing speed (1x, 2x, etc)
49
+ * Search.
50
+ * Pause/rewind/etc live while viewing or recording.
51
+ * Online sharing
52
+ * Live sharing
53
+ * Multiuser
54
+ * Sharing recorded sessions
55
+ * Terminal size options
56
+ * Currently stuck at default 80x24, fix that.
57
+ * Improve & document recording format
58
+ * Currently a sequence of [play_time, length, data].pack("GNA*")
59
+ * Implement a terminal emulator so we can calculate key frames to better support playback/rewind
Binary file
Binary file
data/misc/shatty.go ADDED
@@ -0,0 +1,173 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+ "time"
6
+ "io"
7
+ "os/exec"
8
+ "syscall"
9
+ "unsafe"
10
+ "fmt"
11
+ )
12
+
13
+ /*
14
+ forkpty == openpty + fork
15
+ parent: close slave
16
+ child: close master
17
+
18
+ openpty ==
19
+ master = getpt
20
+ granpt(master)
21
+ unlockpt(master)
22
+ open as file ptsname(master)
23
+ tcsetattr (slave, TCSAFLUSH, termp);
24
+ ioctl (slave, TIOCSWINSZ, winp);
25
+
26
+ getpt
27
+ open /dev/ptmx, return fd (linux specific)
28
+ grantpt
29
+ call ptsname (get the filename of the slave)
30
+ chown/chgrp/chmod the filename to us
31
+ unlockpt
32
+ ioctl(master, TIOCSPTLCK, 0)
33
+ open slave
34
+ if 'login terminal'
35
+ ioctl(slave, TIOCSCTTY, NULL) // set this procses as controlling terminal
36
+ dup stdin/stdout/stderr to slave
37
+ */
38
+
39
+ func getpt() (file *os.File, err error) {
40
+ file, err = os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
41
+ if err != nil {
42
+ return nil, err
43
+ }
44
+ return file, nil
45
+ } /* getpt */
46
+
47
+ func ptsname(file *os.File) (name string, err error) {
48
+ /* On linux, this calls ioctl(fd, TIOCGPTN, ...) */
49
+ var num int
50
+
51
+ /* Get the /dev/pts number */
52
+ err = ioctl(file, syscall.TIOCGPTN, &num)
53
+ if err != nil && err.Error() != "errno 0" {
54
+ return "", err
55
+ }
56
+ return fmt.Sprintf("/dev/pts/%d", num), nil
57
+ }
58
+
59
+ func grantpt(file *os.File) (err error) {
60
+ slave_name, err := ptsname(file)
61
+ if err != nil { return err }
62
+ err = os.Chown(slave_name, os.Getuid(), os.Getgid())
63
+ if err != nil { return err }
64
+ err = os.Chmod(slave_name, 0600)
65
+ if err != nil { return err }
66
+ return nil
67
+ }
68
+
69
+ func unlockpt(file *os.File) (err error) {
70
+ var val = 0
71
+ err = ioctl(file, syscall.TIOCSPTLCK, &val)
72
+
73
+ if err != nil && err.Error() != "errno 0" { return err }
74
+ return nil
75
+ }
76
+
77
+ /* Borrowed with modifications from github.com/kr/pty/pty_linux.go; MIT license */
78
+ func ioctl(file *os.File, command uint, data *int) (err syscall.Errno) {
79
+ _, _, err = syscall.Syscall(syscall.SYS_IOCTL, uintptr(file.Fd()),
80
+ uintptr(command), uintptr(unsafe.Pointer(data)))
81
+ if err != 0 {
82
+ return err
83
+ }
84
+ return syscall.Errno(0)
85
+ } /* ioctl */
86
+
87
+ func openpty() (master *os.File, slave *os.File, err error) {
88
+ master, err = getpt()
89
+ if err != nil { return nil, nil, err }
90
+ if err = grantpt(master); err != nil { return nil, nil, err }
91
+ if err = unlockpt(master); err != nil { return nil, nil, err }
92
+
93
+ slave_name, err := ptsname(master)
94
+ if err != nil { return nil, nil, err }
95
+ slave, err = os.OpenFile(slave_name, os.O_RDWR, 0)
96
+
97
+ return master, slave, nil
98
+ } /* openpty */
99
+
100
+ func dup(file *os.File, name string) (newfile *os.File, err error) {
101
+ fd, err := syscall.Dup(int(file.Fd()))
102
+ if err != nil { return nil, err }
103
+
104
+ return os.NewFile(uintptr(fd), "<stdin>"), nil
105
+ }
106
+
107
+ func forkpty(name string, argv []string, attr *os.ProcAttr) (master *os.File, command *exec.Cmd, err error) {
108
+ master, slave, err := openpty()
109
+ if err != nil { return nil, nil, err }
110
+
111
+ /* dup it up. */
112
+
113
+ fd := [3]*os.File{slave, slave, slave}
114
+ attr.Files = fd[:]
115
+
116
+ command = new(exec.Cmd)
117
+ //command.Path = name
118
+ //command.Args = argv[:]
119
+ command.Stdin, err = dup(slave, "<slave stdin>")
120
+ command.Stdout, err = dup(slave, "<slave stdout>")
121
+ command.Stderr, err = dup(slave, "<slave stderr>")
122
+ //command.Stdout = slave
123
+ //command.Stderr = slave
124
+ command.Process, err = os.StartProcess(name, argv, attr)
125
+ slave.Close()
126
+ if err != nil { return nil, nil, err }
127
+
128
+ /* Now in the parent */
129
+ command.Stdin, err = dup(master, "<stdin>")
130
+ command.Stdout, err = dup(master, "<stdout>")
131
+ command.Stderr, err = dup(master, "<stderr>")
132
+ //command.Stdin = master
133
+ //command.Stdout = master
134
+ //command.Stderr = master
135
+ //master.Close()
136
+
137
+ if err != nil { return nil, nil, err }
138
+ return master, command, nil
139
+ }
140
+
141
+ func main() {
142
+ master, command, err := forkpty("/bin/bash", []string{"/bin/bash", "-li"}, new(os.ProcAttr))
143
+
144
+ if err != nil { fmt.Printf("forkpty: %v\n", err); return }
145
+ fmt.Printf("%T/%v %s\n", master, master, master.Name())
146
+
147
+ go func() {
148
+ for {
149
+ data := make([]byte, 1024)
150
+ _, err := command.Stdout.(io.Reader).Read(data)
151
+ if err != nil { return }
152
+ //fmt.Printf("Read: %d '%#v'\n", size, fmt.Sprintf("%.*s", size, data))
153
+ os.Stdout.Write(data)
154
+ //time.Sleep(500 * time.Millisecond)
155
+ }
156
+ }()
157
+
158
+ time.Sleep(500 * time.Millisecond)
159
+ _, err = command.Stdin.(*os.File).WriteString("echo hello world\n")
160
+ if err != nil { fmt.Printf("Fprintf: %v\n", err); return }
161
+ time.Sleep(500 * time.Millisecond)
162
+ _, err = command.Stdin.(*os.File).WriteString("tty\n")
163
+ if err != nil { fmt.Printf("Fprintf: %v\n", err); return }
164
+ time.Sleep(500 * time.Millisecond)
165
+ //_, err = command.Stdin.(*os.File).WriteString("exit\n")
166
+ //if err != nil { fmt.Printf("Fprintf: %v\n", err); return }
167
+ master.Close()
168
+
169
+ /* It would be nice if this actually closed stdin for the subcommand */
170
+ command.Stdin.(*os.File).Close()
171
+ fmt.Printf("Wait: %v\n", command.Wait())
172
+
173
+ }
data/shatty.rb ADDED
@@ -0,0 +1,115 @@
1
+ require "clamp"
2
+ require "pty"
3
+ require "ftw"
4
+ require "uuidtools"
5
+
6
+ class Shatty < Clamp::Command
7
+ subcommand "record", "Record a command" do
8
+ option ["-o", "--output"], "PATH_OR_URL",
9
+ "where to output the recording to (a path or url)"
10
+ option "--headless", :flag,
11
+ "headless mode; don't output anything to stdout."
12
+ parameter "COMMAND ...", "The command to run",
13
+ :attribute_name => :command
14
+
15
+ def execute
16
+ start = Time.now
17
+
18
+ if output.nil?
19
+ output = "http://r.logstash.net:8200/s/#{UUIDTools::UUID.random_create}"
20
+ puts "Sending output to: #{output}"
21
+ end
22
+
23
+ if output =~ /^https?:/
24
+ agent = FTW::Agent.new
25
+ stream, out = IO::pipe
26
+ Thread.new {
27
+ response = agent.post!(output, :body => stream)
28
+ # TODO(sissel): Shouldn't get here...
29
+ }
30
+ else
31
+ # no http/https, assume file output.
32
+ out = File.new(output, "w")
33
+ end
34
+
35
+ # binary mode.
36
+ buffer = ""
37
+
38
+ STDOUT.sync = true
39
+ terminal, keyboard, pid = PTY.spawn(*command)
40
+ system("stty raw -echo") # yeah, perhaps we should use termios instead.
41
+
42
+ # Dump stdin to the tty's keyboard
43
+ # We could use reopen here, but the 'keyboard' io has mode read/write.
44
+ Thread.new { STDIN.each_char { |c| keyboard.syswrite(c) } }
45
+
46
+ while true
47
+ # Read from the terminal output and compute the time offset
48
+ begin
49
+ terminal.sysread(16834, buffer)
50
+ rescue Errno::EIO => e
51
+ Process.waitpid(pid)
52
+ puts "Command exited with code: #{$?.exitstatus}"
53
+ break
54
+ end
55
+
56
+ time_offset = Time.now - start
57
+
58
+ # for each chunk of text read from tmux, record
59
+ # the timestamp (duration since 'start' of recording)
60
+ out.syswrite([time_offset.to_f, buffer.length, buffer].pack("GNA#{buffer.length}"))
61
+
62
+ $stdout.write(buffer) unless headless?
63
+ end
64
+
65
+ system("stty sane")
66
+ end # def execute
67
+ end # subcommand "record"
68
+
69
+ subcommand "play", "Play a recording" do
70
+ parameter "[PATH_OR_URL]",
71
+ "The recording to play. This can be a path or URL.",
72
+ :attribute_name => :path
73
+
74
+ def execute
75
+ # TODO(sissel): Don't abort :(
76
+ Thread.abort_on_exception = true
77
+
78
+ if path =~ /^https?:/
79
+ input, writer = IO::pipe
80
+ Thread.new do
81
+ agent = FTW::Agent.new
82
+ response = agent.get!(path)
83
+ response.read_http_body { |chunk| writer.syswrite(chunk) }
84
+ end
85
+ else
86
+ input = File.new(path, "w")
87
+ end
88
+
89
+ start = nil
90
+
91
+ $stdout.sync = true
92
+ headersize = [1,1].pack("GN").size
93
+ last_time = 0
94
+ while true
95
+ # Read the header
96
+ begin
97
+ buffer = input.read(headersize)
98
+ time, length = buffer.unpack("GN")
99
+ buffer = input.read(length)
100
+ rescue EOFError
101
+ break
102
+ end
103
+
104
+ # Sleep if necessary
105
+ #sleep(time - last_time) if last_time > 0
106
+ last_time = time
107
+
108
+ # output this frame.
109
+ $stdout.syswrite(buffer)
110
+ end
111
+ end # def execute
112
+ end # subcommand "play"
113
+ end # class Shatty
114
+
115
+ Shatty.run
data/web.rb ADDED
@@ -0,0 +1,84 @@
1
+ require "ftw" # gem ftw
2
+ require "cabin" # gem cabin
3
+ require "thread"
4
+
5
+ ShutdownSignal = :shutdown
6
+
7
+ class Session
8
+ def initialize
9
+ @queue = Queue.new
10
+ @recent = []
11
+ end # def initialize
12
+
13
+ def <<(chunk)
14
+ @queue << chunk
15
+ @recent << chunk
16
+ @recent = @recent[0..100]
17
+ end # def push
18
+
19
+ def enumerator
20
+ return Enumerator.new do |y|
21
+ @recent.each { |chunk| y << chunk }
22
+ while true
23
+ chunk = @queue.pop
24
+ break if chunk == ShutdownSignal
25
+ y << chunk
26
+ end
27
+ end # Enumerator
28
+ end # def enumerator
29
+
30
+ def raw
31
+ return Enumerator.new do |y|
32
+ enumerator.each do |chunk|
33
+ puts decode(chunk).inspect
34
+ y << decode(chunk)
35
+ end
36
+ end # Enumerator
37
+ end # def enumerator
38
+
39
+ def decode(chunk)
40
+ return chunk[headersize .. -1]
41
+ end # def decode
42
+
43
+ def close
44
+ @queue << ShutdownSignal
45
+ end
46
+ end # class Session
47
+
48
+ sessions = {}
49
+
50
+ port = ENV.include?("PORT") ? ENV["PORT"].to_i : 8888
51
+ server = FTW::WebServer.new("0.0.0.0", port) do |request, response|
52
+ @logger = Cabin::Channel.get
53
+ if request.path =~ /^\/s\//
54
+ session = sessions[request.path] ||= Session.new
55
+ if request.method == "POST"
56
+ # TODO(sissel): Check if a session exists.
57
+
58
+ begin
59
+ request.read_http_body do |chunk|
60
+ session << chunk
61
+ end
62
+ rescue EOFError
63
+ end
64
+ session.close
65
+ sessions.delete(request.path)
66
+ elsif request.method == "GET"
67
+ response.status = 200
68
+ response["Content-Type"] = "text/plain"
69
+ if request["user-agent"] =~ /^curl\/[0-9]/
70
+ # Curl. Send raw text.
71
+ response.body = session.raw
72
+ else
73
+ response.body = session.enumerator
74
+ end
75
+ else
76
+ response.status = 400
77
+ response.body = "Invalid method '#{request.method}'\n"
78
+ end
79
+ else
80
+ response.status = 404
81
+ end
82
+ end
83
+
84
+ server.run
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shatty
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jordan Sissel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: cabin
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>'
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>'
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: shatty
31
+ email:
32
+ - jls@semicomplete.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - Gemfile
38
+ - Gemfile.lock
39
+ - Procfile
40
+ - README.md
41
+ - examples/blinkenlights.shatty
42
+ - examples/output.shatty
43
+ - misc/shatty.go
44
+ - shatty.rb
45
+ - web.rb
46
+ homepage:
47
+ licenses:
48
+ - none chosen yet
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.24
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: shatty
72
+ test_files: []