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:
60
services/client.go
Normal file
60
services/client.go
Normal 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
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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]
|
||||
}
|
||||
|
Reference in New Issue
Block a user