1
0
mirror of https://github.com/bingohuang/docker-labs.git synced 2025-10-04 17:33:21 +08:00

Huge refactor to have everything working with socket.io

It fixes lots of bugs, can fallback to long polling, resize viewport of
terminals and share clients state of the session, so they all see the
same thing.
This commit is contained in:
Jonathan Leibiusky (@xetorthio)
2016-11-10 10:42:08 -03:00
parent 9b6991f130
commit 8e4981d24f
16 changed files with 566 additions and 249 deletions

60
services/client.go Normal file
View File

@@ -0,0 +1,60 @@
package services
import "github.com/googollee/go-socket.io"
type ViewPort struct {
Rows uint
Cols uint
}
type Client struct {
SO socketio.Socket
ViewPort ViewPort
}
func (c *Client) ResizeViewPort(cols, rows uint) {
c.ViewPort.Rows = rows
c.ViewPort.Cols = cols
}
func NewClient(so socketio.Socket, session *Session) *Client {
so.Join(session.Id)
c := &Client{SO: so}
so.On("terminal in", func(name, data string) {
// User wrote something on the terminal. Need to write it to the instance terminal
instance := GetInstance(session, name)
if len(data) > 0 {
instance.Conn.Conn.Write([]byte(data))
}
})
so.On("viewport resize", func(cols, rows uint) {
// User resized his viewport
c.ResizeViewPort(cols, rows)
vp := session.GetSmallestViewPort()
// Resize all terminals in the session
wsServer.BroadcastTo(session.Id, "viewport resize", vp.Cols, vp.Rows)
for _, instance := range session.Instances {
instance.ResizeTerminal(vp.Cols, vp.Rows)
}
})
so.On("disconnection", func() {
// Client has disconnected. Remove from session and recheck terminal sizes.
for i, cl := range session.Clients {
if cl.SO.Id() == c.SO.Id() {
session.Clients = append(session.Clients[:i], session.Clients[i+1:]...)
break
}
}
vp := session.GetSmallestViewPort()
// Resize all terminals in the session
wsServer.BroadcastTo(session.Id, "viewport resize", vp.Cols, vp.Rows)
for _, instance := range session.Instances {
instance.ResizeTerminal(vp.Cols, vp.Rows)
}
})
return c
}

View File

@@ -4,8 +4,6 @@ import (
"log"
"strings"
ptypes "github.com/franela/play-with-docker/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
@@ -29,9 +27,7 @@ func GetContainerInfo(id string) (types.ContainerJSON, error) {
}
func CreateNetwork(name string) error {
// TODO: This line appears to give an error when running on localhost:3000
// when driver is specified a name must be given.
opts := types.NetworkCreate{Attachable: true, Driver: "overlay"}
opts := types.NetworkCreate{}
_, err := c.NetworkCreate(context.Background(), name, opts)
if err != nil {
@@ -71,16 +67,14 @@ func AttachExecConnection(execId string, ctx context.Context) (*types.HijackedRe
return nil, err
}
err = c.ContainerExecResize(ctx, execId, types.ResizeOptions{Height: 24, Width: 80})
if err != nil {
return nil, err
}
return &conn, nil
}
func CreateInstance(net string, dindImage string) (*ptypes.Instance, error) {
func ResizeExecConnection(execId string, ctx context.Context, cols, rows uint) error {
return c.ContainerExecResize(ctx, execId, types.ResizeOptions{Height: rows, Width: cols})
}
func CreateInstance(net string, dindImage string) (*Instance, error) {
var maximumPidLimit int64
maximumPidLimit = 150 // Set a ulimit value to prevent misuse
@@ -104,7 +98,7 @@ func CreateInstance(net string, dindImage string) (*ptypes.Instance, error) {
return nil, err
}
return &ptypes.Instance{Name: strings.Replace(cinfo.Name, "/", "", 1), IP: cinfo.NetworkSettings.Networks[net].IPAddress}, nil
return &Instance{Name: strings.Replace(cinfo.Name, "/", "", 1), IP: cinfo.NetworkSettings.Networks[net].IPAddress}, nil
}
func DeleteContainer(id string) error {

View File

@@ -1,12 +1,25 @@
package services
import (
"context"
"io"
"log"
"os"
"github.com/franela/play-with-docker/types"
"golang.org/x/text/encoding"
"github.com/docker/docker/api/types"
)
type Instance struct {
Session *Session `json:"-"`
Name string `json:"name"`
IP string `json:"ip"`
Conn *types.HijackedResponse `json:"-"`
ExecId string `json:"-"`
Ctx context.Context `json:"-"`
}
var dindImage string
var defaultDindImageName string
@@ -23,31 +36,78 @@ func getDindImageName() string {
return dindImage
}
func NewInstance(session *types.Session) (*types.Instance, error) {
func NewInstance(session *Session) (*Instance, error) {
//TODO: Validate that a session can only have 5 instances
//TODO: Create in redis
log.Printf("NewInstance - using image: [%s]\n", dindImage)
instance, err := CreateInstance(session.Id, dindImage)
instance.Session = session
if err != nil {
return nil, err
}
if session.Instances == nil {
session.Instances = make(map[string]*types.Instance)
session.Instances = make(map[string]*Instance)
}
session.Instances[instance.Name] = instance
go instance.Exec()
wsServer.BroadcastTo(session.Id, "new instance", instance.Name, instance.IP)
return instance, nil
}
func GetInstance(session *types.Session, name string) *types.Instance {
type sessionWriter struct {
instance *Instance
}
func (s *sessionWriter) Write(p []byte) (n int, err error) {
wsServer.BroadcastTo(s.instance.Session.Id, "terminal out", s.instance.Name, string(p))
return len(p), nil
}
func (i *Instance) ResizeTerminal(cols, rows uint) {
ResizeExecConnection(i.ExecId, i.Ctx, cols, rows)
}
func (i *Instance) Exec() {
i.Ctx = context.Background()
id, err := CreateExecConnection(i.Name, i.Ctx)
if err != nil {
return
}
i.ExecId = id
conn, err := AttachExecConnection(id, i.Ctx)
if err != nil {
return
}
i.Conn = conn
go func() {
encoder := encoding.Replacement.NewEncoder()
sw := &sessionWriter{instance: i}
io.Copy(encoder.Writer(sw), conn.Reader)
}()
select {
case <-i.Ctx.Done():
}
}
func GetInstance(session *Session, name string) *Instance {
//TODO: Use redis
return session.Instances[name]
}
func DeleteInstance(session *types.Session, instance *types.Instance) error {
func DeleteInstance(session *Session, instance *Instance) error {
//TODO: Use redis
delete(session.Instances, instance.Name)
return DeleteContainer(instance.Name)
err := DeleteContainer(instance.Name)
wsServer.BroadcastTo(session.Id, "delete instance", instance.Name)
return err
}

View File

@@ -2,30 +2,66 @@ package services
import (
"log"
"math"
"sync"
"time"
"github.com/franela/play-with-docker/types"
"github.com/googollee/go-socket.io"
"github.com/twinj/uuid"
)
var sessions map[string]*types.Session
var wsServer *socketio.Server
func init() {
sessions = make(map[string]*types.Session)
type Session struct {
sync.Mutex
Id string `json:"id"`
Instances map[string]*Instance `json:"instances"`
Clients []*Client `json:"-"`
}
func NewSession() (*types.Session, error) {
s := &types.Session{}
func (s *Session) GetSmallestViewPort() ViewPort {
minRows := s.Clients[0].ViewPort.Rows
minCols := s.Clients[0].ViewPort.Cols
for _, c := range s.Clients {
minRows = uint(math.Min(float64(minRows), float64(c.ViewPort.Rows)))
minCols = uint(math.Min(float64(minCols), float64(c.ViewPort.Cols)))
}
return ViewPort{Rows: minRows, Cols: minCols}
}
func (s *Session) AddNewClient(c *Client) {
s.Clients = append(s.Clients, c)
}
var sessions map[string]*Session
func init() {
sessions = make(map[string]*Session)
}
func CreateWSServer() *socketio.Server {
server, err := socketio.NewServer(nil)
if err != nil {
log.Fatal(err)
}
wsServer = server
return server
}
func NewSession() (*Session, error) {
s := &Session{}
s.Id = uuid.NewV4().String()
s.Instances = map[string]*types.Instance{}
s.Instances = map[string]*Instance{}
log.Printf("NewSession id=[%s]\n", s.Id)
//TODO: Store in something like redis
sessions[s.Id] = s
// Schedule cleanup of the session
time.AfterFunc(1*time.Hour, func() {
s = GetSession(s.Id)
wsServer.BroadcastTo(s.Id, "session end")
log.Printf("Starting clean up of session [%s]\n", s.Id)
for _, i := range s.Instances {
i.Conn.Close()
@@ -41,13 +77,13 @@ func NewSession() (*types.Session, error) {
})
if err := CreateNetwork(s.Id); err != nil {
log.Println("ERROR NETWORKING")
return nil, err
}
return s, nil
}
func GetSession(sessionId string) *types.Session {
//TODO: Use redis
func GetSession(sessionId string) *Session {
return sessions[sessionId]
}