shatty 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []