/
terminal.go
130 lines (106 loc) · 2.69 KB
/
terminal.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package terminal
import (
"io"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
"github.com/asciinema/asciinema/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal"
"github.com/asciinema/asciinema/Godeps/_workspace/src/github.com/creack/termios/raw"
"github.com/asciinema/asciinema/Godeps/_workspace/src/github.com/kr/pty"
"github.com/asciinema/asciinema/Godeps/_workspace/src/golang.org/x/text/encoding/unicode"
"github.com/asciinema/asciinema/Godeps/_workspace/src/golang.org/x/text/transform"
"github.com/asciinema/asciinema/ptyx"
"github.com/asciinema/asciinema/util"
)
type Terminal interface {
Size() (int, int, error)
Record(string, io.Writer) error
Write([]byte) error
}
type Pty struct {
Stdin *os.File
Stdout *os.File
}
func NewTerminal() Terminal {
return &Pty{Stdin: os.Stdin, Stdout: os.Stdout}
}
func (p *Pty) Size() (int, int, error) {
return pty.Getsize(p.Stdout)
}
func (p *Pty) Record(command string, w io.Writer) error {
// start command in pty
cmd := exec.Command("sh", "-c", command)
cmd.Env = append(os.Environ(), "ASCIINEMA_REC=1")
master, err := pty.Start(cmd)
if err != nil {
return err
}
defer master.Close()
// install WINCH signal handler
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGWINCH)
defer signal.Stop(signals)
go func() {
for _ = range signals {
p.resize(master)
}
}()
defer close(signals)
// put stdin in raw mode (if it's a tty)
fd := p.Stdin.Fd()
if terminal.IsTerminal(int(fd)) {
oldState, err := raw.MakeRaw(fd)
if err != nil {
return err
}
defer raw.TcSetAttr(fd, oldState)
}
// do initial resize
p.resize(master)
// start stdin -> master copying
stop := util.Copy(master, p.Stdin)
// copy pty master -> p.stdout & w
stdout := transform.NewWriter(w, unicode.UTF8.NewEncoder())
defer stdout.Close()
stdoutWaitChan := make(chan struct{})
go func() {
io.Copy(io.MultiWriter(p.Stdout, stdout), master)
stdoutWaitChan <- struct{}{}
}()
// wait for the process to exit and reap it
cmd.Wait()
// wait for master -> stdout copying to finish
//
// sometimes after process exits reading from master blocks forever (race condition?)
// we're using timeout here to overcome this problem
select {
case <-stdoutWaitChan:
case <-time.After(200 * time.Millisecond):
}
// stop stdin -> master copying
stop()
return nil
}
func (p *Pty) Write(data []byte) error {
_, err := p.Stdout.Write(data)
if err != nil {
return err
}
err = p.Stdout.Sync()
if err != nil {
return err
}
return nil
}
func (p *Pty) resize(f *os.File) {
var rows, cols int
if terminal.IsTerminal(int(p.Stdout.Fd())) {
rows, cols, _ = p.Size()
} else {
rows = 24
cols = 80
}
ptyx.Setsize(f, rows, cols)
}