Files
teleport/lib/client/player.go
T
Russell Jones a4b070c750 Fixed vendoring issues.
* Changed import path from github.com/moby/moby to canonical path of
    github.com/docker/docker.
  * Updated dependency for github.com/docker/docker/pkg/term.
  * Updated dependency for github.com/Azure/go-ansiterm.
2018-07-25 13:51:50 -07:00

218 lines
4.8 KiB
Go

/*
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package client
import (
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/docker/docker/pkg/term"
"github.com/gravitational/teleport/lib/events"
)
const (
stateStopped = iota
stateStopping
statePlaying
)
// sessionPlayer implements replaying terminal sessions. It runs a playback goroutine
// and allows to control it
type sessionPlayer struct {
sync.Mutex
stream []byte
sessionEvents []events.EventFields
state int
position int
// stopC is used to tell the caller that player has finished playing
stopC chan int
}
func newSessionPlayer(sessionEvents []events.EventFields, stream []byte) *sessionPlayer {
return &sessionPlayer{
stream: stream,
sessionEvents: sessionEvents,
stopC: make(chan int, 0),
}
}
func (p *sessionPlayer) Play() {
p.playRange(0, 0)
}
func (p *sessionPlayer) Stop() {
p.Lock()
defer p.Unlock()
if p.stopC != nil {
close(p.stopC)
p.stopC = nil
}
}
func (p *sessionPlayer) Stopped() bool {
return p.state == stateStopped
}
func (p *sessionPlayer) Rewind() {
p.Lock()
defer p.Unlock()
if p.state != stateStopped {
p.state = stateStopping
p.waitUntil(stateStopped)
}
if p.position > 0 {
p.playRange(p.position-1, p.position)
}
}
func (p *sessionPlayer) Forward() {
p.Lock()
defer p.Unlock()
if p.state != stateStopped {
p.state = stateStopping
p.waitUntil(stateStopped)
}
if p.position < len(p.sessionEvents) {
p.playRange(p.position+2, p.position+2)
}
}
func (p *sessionPlayer) TogglePause() {
p.Lock()
defer p.Unlock()
if p.state == statePlaying {
p.state = stateStopping
p.waitUntil(stateStopped)
} else {
p.playRange(p.position, 0)
p.waitUntil(statePlaying)
}
}
func (p *sessionPlayer) waitUntil(state int) {
for state != p.state {
time.Sleep(time.Millisecond)
}
}
// timestampFrame prints 'event timestamp' in the top right corner of the
// terminal after playing every 'print' event
func timestampFrame(message string) {
const (
saveCursor = "7"
restoreCursor = "8"
)
sz, err := term.GetWinsize(0)
if err != nil {
return
}
esc := func(s string) {
os.Stdout.Write([]byte("\x1b" + s))
}
esc(saveCursor)
defer esc(restoreCursor)
// move cursor to -10:0
esc(fmt.Sprintf("[%d;%df", 0, int(sz.Width)-len(message)))
os.Stdout.WriteString(message)
}
// playRange plays events from a given from:to range. In order for the replay
// to render correctly, playRange always plays from the beginning, but starts
// applying timing info (delays) only after 'from' event, creating an impression
// that playback starts from there.
func (p *sessionPlayer) playRange(from, to int) {
if to > len(p.sessionEvents) || from < 0 {
p.state = stateStopped
return
}
if to == 0 {
to = len(p.sessionEvents)
}
// clear screen between runs:
os.Stdout.Write([]byte("\x1bc"))
// wait: waits between events during playback
prev := time.Duration(0)
wait := func(i int, e events.EventFields) {
ms := time.Duration(e.GetInt("ms"))
// before "from"? play that instantly:
if i >= from {
delay := ms - prev
// make playback smoother:
if delay < 10 {
delay = 0
}
if delay > 250 && delay < 500 {
delay = 250
}
if delay > 500 && delay < 1000 {
delay = 500
}
if delay > 1000 {
delay = 1000
}
timestampFrame(e.GetString("time"))
time.Sleep(time.Millisecond * delay)
}
prev = ms
}
// playback goroutine:
go func() {
defer func() {
p.state = stateStopped
}()
p.state = statePlaying
i, offset, bytes := 0, 0, 0
for i = 0; i < to; i++ {
if p.state == stateStopping {
return
}
e := p.sessionEvents[i]
switch e.GetString(events.EventType) {
// 'print' event (output)
case events.SessionPrintEvent:
wait(i, e)
offset = e.GetInt("offset")
bytes = e.GetInt("bytes")
os.Stdout.Write(p.stream[offset : offset+bytes])
// resize terminal event (also on session start)
case events.ResizeEvent, events.SessionStartEvent:
parts := strings.Split(e.GetString("size"), ":")
if len(parts) != 2 {
continue
}
width, height := parts[0], parts[1]
// resize terminal window by sending control sequence:
os.Stdout.Write([]byte(fmt.Sprintf("\x1b[8;%s;%st", height, width)))
default:
continue
}
p.position = i
}
// played last event?
if i == len(p.sessionEvents) {
p.Stop()
}
}()
}