gymvnc/gymvnc.go (555 lines of code) (raw):
package gymvnc
import (
"fmt"
"net"
"sync"
"time"
"github.com/juju/errors"
logging "github.com/op/go-logging"
"github.com/openai/go-vncdriver/vncclient"
)
var (
log = logging.MustGetLogger("gymvnc")
id = 0
)
type sessionMgr struct {
Ready sync.WaitGroup
Done chan bool
Error chan error
Terminated sync.WaitGroup
}
func NewSessionMgr() *sessionMgr {
return &sessionMgr{
Done: make(chan bool),
Error: make(chan error, 1),
}
}
func (s *sessionMgr) Close() error {
close(s.Done)
return nil
}
type Region struct {
X, Y, Width, Height uint16
}
type VNCSessionConfig struct {
Address string
Password string
Encoding string
QualityLevel int // 0-9, 9 being top quality. Not orthogonal to FineQualityLevel/SubsampleLevel, see https://github.com/TurboVNC/turbovnc/blob/master/unix/Xvnc/programs/Xserver/hw/vnc/rfbserver.c#L1103-L1112
CompressLevel int // 0-9, 9 being highest compression
FineQualityLevel int // 0-100, 100 being top quality
SubsampleLevel int // 0-3, 3 being grayscale; 0 being full color
StartTimeout time.Duration
Subscription []Region
}
type VNCSession struct {
id int
label string
mgr *sessionMgr
conn *vncclient.ClientConn
frontScreen *Screen
backScreen *Screen
backUpdated bool
deferredUpdates []*vncclient.FramebufferUpdateMessage
deferredUpdatesMax int
pauseUpdates bool
updated *sync.Cond
renderer Renderer
rendererActive bool
name string
config VNCSessionConfig
lock sync.Mutex
err error
closed bool
}
func NewVNCSession(name string, c VNCSessionConfig) *VNCSession {
if c.QualityLevel == -1 {
} else if c.QualityLevel < 0 {
log.Warningf("[%s] Quality level %d requested, but valid values are betweeen 0 (worst) and 9 (best). Using 0 intead.", c.Address, c.QualityLevel)
c.QualityLevel = 0
} else if c.QualityLevel > 9 {
log.Warningf("[%s] Quality level %d requested, but valid values are betweeen 0 (worst) and 9 (best). Using 9 instead.", c.Address, c.QualityLevel)
c.QualityLevel = 9
}
if c.CompressLevel == -1 {
} else if c.CompressLevel < 0 {
log.Warningf("[%s] Compress level %d requested, but valid values are betweeen 0 (least compression) and 9 (most compression). Using 0 intead.", c.Address, c.CompressLevel)
c.CompressLevel = 0
} else if c.CompressLevel > 9 {
log.Warningf("[%s] Compress level %d requested, but valid values are betweeen 0 (least compression) and 9 (most compression). Using 9 instead.", c.Address, c.CompressLevel)
c.CompressLevel = 9
}
if c.FineQualityLevel == -1 {
} else if c.FineQualityLevel < 0 {
log.Warningf("[%s] Fine quality level %d requested, but valid values are betweeen 1 (lowest quality) and 100 (highest quality). Using 0 instead.", c.Address, c.FineQualityLevel)
c.FineQualityLevel = 0
} else if c.FineQualityLevel > 100 {
log.Warningf("[%s] Fine quality level %d requested, but valid values are betweeen 0 (lowest quality) and 100 (highest quality). Using 100 instead.", c.Address, c.FineQualityLevel)
c.FineQualityLevel = 100
}
if c.SubsampleLevel == -1 {
} else if c.SubsampleLevel < 0 {
log.Warningf("[%s] Subsample level %d requested, but valid values are betweeen 0 (full color) and 3 (grayscale). Using 0 instead.", c.Address, c.SubsampleLevel)
c.SubsampleLevel = 0
} else if c.SubsampleLevel > 3 {
log.Warningf("[%s] Subsample level %d requested, but valid values are betweeen 0 (full color) and 3 (grayscale). Using 3 instead.", c.Address, c.SubsampleLevel)
c.SubsampleLevel = 3
}
if c.Encoding == "" {
c.Encoding = "tight"
}
lock := &sync.Mutex{}
session := &VNCSession{
name: name,
mgr: NewSessionMgr(),
backUpdated: true,
deferredUpdatesMax: 60,
updated: sync.NewCond(lock),
config: c,
id: id,
label: fmt.Sprintf("%s:%s", name, c.Address),
}
id++
session.start()
return session
}
func (c *VNCSession) start() {
updates := make(chan *vncclient.FramebufferUpdateMessage, 4096)
// Called from main thread. Will call Done once this session
// is fully setup.
c.mgr.Ready.Add(1)
// Maintains the connection to the remote
go func() {
err := c.connect(updates)
if err != nil {
// Try reporting the error
select {
case c.mgr.Error <- err:
default:
}
}
}()
go func() {
if err := <-c.mgr.Error; err != nil {
c.lock.Lock()
c.err = err
c.lock.Unlock()
}
}()
}
func (c *VNCSession) Close() error {
c.lock.Lock()
defer c.lock.Unlock()
if c.closed {
return nil
}
c.closed = true
c.mgr.Close()
if c.conn != nil {
log.Debugf("[%s] closing connection to VNC server", c.label)
c.conn.Close()
}
if c.rendererActive {
// This can *only* be called from the main thread, so
// we can't auto-clean it up on error.
c.renderer.Close()
}
return nil
}
func (c *VNCSession) Step(events []VNCEvent) (*Screen, []*vncclient.FramebufferUpdateMessage, error) {
c.lock.Lock()
conn := c.conn
err := c.err
c.lock.Unlock()
if err != nil {
// this VNCSession is broken
return nil, nil, err
} else if conn == nil {
// not yet connected
return nil, nil, nil
}
for _, event := range events {
err := event.Execute(conn)
if err != nil {
return nil, nil, errors.Annotatef(err, "could not step %s", c.config.Address)
}
}
screen, updates := c.Flip()
return screen, updates, nil
}
func (c *VNCSession) SetRenderer(renderer Renderer) error {
if c.rendererActive {
return errors.New("cannot change renderer while active")
}
c.renderer = renderer
return nil
}
// Must call from main thread
func (c *VNCSession) Render(close bool) error {
if c.renderer == nil {
return errors.Errorf("[%s] VNCSession has no renderer. This likely means your go_vncdriver was installed without the OpenGL viewer. See https://github.com/openai/go-vncdriver for details on how debug.", c.label)
}
c.lock.Lock()
if c.closed {
c.lock.Unlock()
return errors.Errorf("[%s] VNCSession is already closed; can't render", c.label)
}
conn := c.conn
c.lock.Unlock()
if conn == nil {
// Not connected yet; nothing to render!
return nil
}
if !c.rendererActive {
if err := c.renderer.Init(c.conn.FramebufferWidth, c.conn.FramebufferHeight, "go-vncdriver: "+c.conn.DesktopName, c.frontScreen.Data); err != nil {
return errors.Annotate(err, "could not render")
}
c.rendererActive = true
}
if close {
c.renderer.Close()
} else {
c.renderer.Render()
}
return nil
}
// If rendering, must call from main thread
func (c *VNCSession) Flip() (*Screen, []*vncclient.FramebufferUpdateMessage) {
c.updated.L.Lock()
defer c.updated.L.Unlock()
var updates []*vncclient.FramebufferUpdateMessage
if c.backUpdated {
c.frontScreen, c.backScreen = c.backScreen, c.frontScreen
c.backUpdated = false
updates = c.deferredUpdates
go func() {
c.updated.L.Lock()
defer c.updated.L.Unlock()
c.applyDeferred()
if c.pauseUpdates {
// Restart the framebuffer request cycle
c.pauseUpdates = false
log.Infof("[%s] resuming updates", c.label)
err := c.requestUpdate()
if err != nil {
select {
case c.mgr.Error <- err:
default:
}
}
}
}()
if c.rendererActive {
// Keep the GL screen fed
c.renderer.Apply(updates)
}
}
return c.frontScreen, updates
}
// Apply any deferred updates *while holding the lock*
func (c *VNCSession) applyDeferred() error {
if c.backUpdated {
return nil
}
c.backUpdated = true
for _, update := range c.deferredUpdates {
c.applyUpdate(update)
}
c.deferredUpdates = nil
return nil
}
// Apply an update *while holding the lock*
func (c *VNCSession) applyUpdate(update *vncclient.FramebufferUpdateMessage) error {
var bytes uint32
start := time.Now().UnixNano()
for _, rect := range update.Rectangles {
switch enc := rect.Enc.(type) {
case *vncclient.RawEncoding:
bytes += c.applyRect(c.conn, rect, enc.Colors)
case *vncclient.ZRLEEncoding:
bytes += c.applyRect(c.conn, rect, enc.Colors)
case *vncclient.TightEncoding:
bytes += c.applyRect(c.conn, rect, enc.Colors)
default:
return errors.Errorf("unsupported encoding: %T", enc)
}
}
delta := time.Now().UnixNano() - start
log.Debugf("[%s] Update complete: time=%dus type=%T rectangles=%+v bytes=%d", c.label, delta/1000, update, len(update.Rectangles), bytes)
return nil
}
func (c *VNCSession) applyRect(conn *vncclient.ClientConn, rect vncclient.Rectangle, colors []vncclient.Color) uint32 {
var bytes uint32
// var wg sync.WaitGroup
// wg.Add(int(rect.Height))
for y := uint32(0); y < uint32(rect.Height); y++ {
// go func(y uint32) {
encStart := uint32(rect.Width) * y
encEnd := encStart + uint32(rect.Width)
screenStart := uint32(conn.FramebufferWidth)*(uint32(rect.Y)+y) + uint32(rect.X)
screenEnd := screenStart + uint32(rect.Width)
bytes += encEnd - encStart
copy(c.backScreen.Data[screenStart:screenEnd], colors[encStart:encEnd])
// wg.Done()
// }(y)
}
// wg.Wait()
return bytes
}
func (c *VNCSession) maintainFrameBuffer(updates chan *vncclient.FramebufferUpdateMessage) error {
done := false
for {
select {
case update := <-updates:
c.updated.L.Lock()
if err := c.applyDeferred(); err != nil {
c.updated.L.Unlock()
return errors.Annotate(err, "when applying deferred updates")
}
if err := c.applyUpdate(update); err != nil {
c.updated.L.Unlock()
return errors.Annotate(err, "when applying new update")
}
c.deferredUpdates = append(c.deferredUpdates, update)
if len(c.deferredUpdates) >= c.deferredUpdatesMax && !c.pauseUpdates {
log.Infof("[%s] update queue max of %d reached; pausing further updates", c.label, c.deferredUpdatesMax)
c.pauseUpdates = true
}
// Update complete!
c.updated.Broadcast()
c.updated.L.Unlock()
case <-c.mgr.Done:
log.Debugf("[%s] shutting down frame buffer thread", c.label)
return nil
}
if !done {
c.mgr.Ready.Done()
done = true
}
}
}
func (c *VNCSession) SetSubscription(subs []Region) {
c.lock.Lock()
defer c.lock.Unlock()
c.config.Subscription = subs
}
func (c *VNCSession) requestUpdate() error {
if c.config.Subscription != nil {
for _, sub := range c.config.Subscription {
err := c.conn.FramebufferUpdateRequest(true, sub.X, sub.Y, sub.Width, sub.Height)
if err != nil {
return err
}
}
} else {
err := c.conn.FramebufferUpdateRequest(true, 0, 0, c.conn.FramebufferWidth, c.conn.FramebufferHeight)
if err != nil {
return err
}
}
return nil
}
func (c *VNCSession) connect(updates chan *vncclient.FramebufferUpdateMessage) error {
log.Infof("[%s] opening connection to VNC server", c.label)
errorCh := make(chan error, 1)
serverMessageCh := make(chan vncclient.ServerMessage)
var conn *vncclient.ClientConn
totalSleep := 0 * time.Second
for i := 0; ; i++ {
soft := true
target, err := net.Dial("tcp", c.config.Address)
if err == nil {
conn, err, soft = vncclient.Client(target, &vncclient.ClientConfig{
Auth: []vncclient.ClientAuth{
&vncclient.PasswordAuth{
Password: c.config.Password,
},
},
ServerMessageCh: serverMessageCh,
ErrorCh: errorCh,
})
}
if soft == true && c.config.StartTimeout > 0 {
if totalSleep >= c.config.StartTimeout {
return errors.Annotatef(err, "could not establish VNC connection to server, and exceeded start timeout of %s by sleeping for %s", c.config.StartTimeout, totalSleep)
}
// Choose how long to sleep
sleepTarget := time.Duration(2*(i+1)) * time.Second
if sleepTarget > time.Duration(30)*time.Second {
sleepTarget = time.Duration(30) * time.Second
}
log.Infof("[%s] Waiting on VNC server: %s. Retrying in %s (%s/%s)", c.label, err, sleepTarget, totalSleep, c.config.StartTimeout)
totalSleep += sleepTarget
time.Sleep(sleepTarget)
} else if err != nil {
return errors.Annotate(err, "could not establish VNC connection to server")
} else {
break
}
}
defer func() {
c.lock.Lock()
if c.conn == nil {
// We never stored the conn object, so we need
// to close it ourselves.
log.Infof("[%s] autoclosing connection to VNC server", c.label)
conn.Close()
}
c.lock.Unlock()
}()
go func() {
select {
case err := <-errorCh:
c.mgr.Error <- errors.Annotatef(err, "[%s] vnc error", c.label)
case <-c.mgr.Done:
}
}()
// While the VNC protocol supports more exotic formats, we
// only want straight RGB with 1 byte per color.
c.frontScreen = NewScreen(conn.FramebufferWidth, conn.FramebufferHeight)
c.backScreen = NewScreen(conn.FramebufferWidth, conn.FramebufferHeight)
err := conn.SetPixelFormat(&vncclient.PixelFormat{
BPP: 32,
Depth: 24,
BigEndian: false,
TrueColor: true,
RedMax: 255,
GreenMax: 255,
BlueMax: 255,
RedShift: 0,
GreenShift: 8,
BlueShift: 16,
})
if err != nil {
return errors.Annotate(err, "could not set pixel format")
}
var encoding vncclient.Encoding
switch c.config.Encoding {
case "tight":
encoding = &vncclient.TightEncoding{}
case "zrle":
encoding = &vncclient.ZRLEEncoding{}
case "raw":
encoding = &vncclient.RawEncoding{}
default:
return errors.Errorf("invalid encoding: %s", c.config.Encoding)
}
encodings := []vncclient.Encoding{
encoding,
}
if c.config.QualityLevel != -1 {
encodings = append(encodings, vncclient.QualityLevel(c.config.QualityLevel))
}
if c.config.CompressLevel != -1 {
encodings = append(encodings, vncclient.CompressLevel(c.config.CompressLevel))
}
if c.config.FineQualityLevel != -1 {
encodings = append(encodings, vncclient.FineQualityLevel(c.config.FineQualityLevel))
}
if c.config.SubsampleLevel != -1 {
encodings = append(encodings, vncclient.SubsampleLevel(c.config.SubsampleLevel))
}
err = conn.SetEncodings(encodings)
if err != nil {
return errors.Annotate(err, "could not set encodings")
}
c.lock.Lock()
// Make the connection visible so it can be used in requestUpdate
c.conn = conn
err = c.requestUpdate()
if err != nil {
c.conn = nil
c.lock.Unlock()
return errors.Annotate(err, "could not send framebuffer update request")
}
if c.closed {
c.conn = nil
c.lock.Unlock()
// Conn will be closed by our earlier defer
return errors.Errorf("[%s] VNCSession object was closed before connection was established", c.label)
}
log.Infof("[%s] connection established", c.label)
c.lock.Unlock()
// Spin up a screenbuffer thread
go func() {
err := c.maintainFrameBuffer(updates)
if err != nil {
// Report the error, if any
select {
case c.mgr.Error <- err:
default:
}
}
}()
for {
select {
case msg := <-serverMessageCh:
log.Debugf("[%s] Just received: %T %+v", c.label, msg, msg)
switch msg := msg.(type) {
case *vncclient.FramebufferUpdateMessage:
updates <- msg
// Keep re-requesting!
c.updated.L.Lock()
if !c.pauseUpdates {
err := c.requestUpdate()
if err != nil {
select {
case c.mgr.Error <- err:
default:
}
}
}
c.updated.L.Unlock()
}
case <-c.mgr.Done:
log.Debugf("[%s] server message goroutine exiting", c.label)
return nil
}
}
}
type VNCBatch struct {
sessions map[string]*VNCSession
}
func NewVNCBatch() *VNCBatch {
return &VNCBatch{
sessions: map[string]*VNCSession{},
}
}
func (v *VNCBatch) Close(name string) error {
if session, ok := v.sessions[name]; ok {
session.Close()
delete(v.sessions, name)
}
return nil
}
func (v *VNCBatch) Open(name string, config VNCSessionConfig) error {
if evicted, ok := v.sessions[name]; ok {
evicted.Close()
}
session := NewVNCSession(name, config)
v.sessions[name] = session
return nil
}
func (v *VNCBatch) Step(actions map[string][]VNCEvent) (map[string]*Screen, map[string][]*vncclient.FramebufferUpdateMessage, map[string]error) {
observationN := map[string]*Screen{}
updatesN := map[string][]*vncclient.FramebufferUpdateMessage{}
errN := map[string]error{}
for name, action := range actions {
session := v.sessions[name]
observation, updates, err := session.Step(action)
observationN[name] = observation
updatesN[name] = updates
errN[name] = err
}
return observationN, updatesN, errN
}
func (v *VNCBatch) SetSubscription(name string, subs []Region) error {
if session, ok := v.sessions[name]; ok {
session.SetSubscription(subs)
return nil
} else {
return errors.Errorf("no such session: %s", name)
}
}
func (v *VNCBatch) SetRenderer(name string, renderer Renderer) error {
if session, ok := v.sessions[name]; ok {
return session.SetRenderer(renderer)
} else {
return errors.Errorf("no such session: %s", name)
}
}
func (v *VNCBatch) Render(name string, close bool) error {
if session, ok := v.sessions[name]; ok {
return session.Render(close)
} else {
return errors.Errorf("no such session: %s", name)
}
}
func (v *VNCBatch) Flip() (screens []*Screen, updates [][]*vncclient.FramebufferUpdateMessage) {
for _, session := range v.sessions {
screen, update := session.Flip()
screens = append(screens, screen)
updates = append(updates, update)
}
return
}
func (v *VNCBatch) Peek() (screens []*Screen) {
for _, session := range v.sessions {
screens = append(screens, session.frontScreen)
}
return
}
func (v *VNCBatch) PeekBack() (screens []*Screen) {
for _, session := range v.sessions {
screens = append(screens, session.backScreen)
}
return
}
// func NewVNCBatch(addresses []string, passwords []string, compressLevel, fineQualityLevel, subsampleLevel int, encoding string, done chan bool, errCh chan error) (*VNCBatch, error) {
// batch := &VNCBatch{}
// mgr := NewSessionMgr()
// for i := range addresses {
// address := addresses[i]
// password := passwords[i]
// batch.sessions = append(batch.sessions, NewVNCSession(address, password, compressLevel, fineQualityLevel, subsampleLevel, encoding, mgr))
// }
// allReady := make(chan bool)
// go func() {
// mgr.Ready.Wait()
// allReady <- true
// }()
// select {
// case <-allReady:
// go func() {
// select {
// case <-done:
// log.Debugf("Closing VNC batch due to user request")
// // Translate 'done' closing into closing down
// // this pipeline.
// close(mgr.Done)
// case err := <-mgr.Error:
// log.Debugf("Closing VNCBatch due to error: %s", err)
// // Capture the relevant error, and let
// // the user know.
// batch.setError(err)
// errCh <- err
// close(mgr.Done)
// }
// }()
// return batch, nil
// case err := <-mgr.Error:
// return nil, err
// case <-done:
// // upstream requested a cancelation
// mgr.Close()
// return nil, nil
// }
// }