From 23bb4eaf691958a3010e1c967fd170bb9da0281e Mon Sep 17 00:00:00 2001 From: Marcos Lilljedahl Date: Sun, 13 Nov 2016 20:13:39 -0300 Subject: [PATCH] Add session persistence and use attach instead of exec --- Dockerfile.dind | 1 + api.go | 7 +++++ services/client.go | 17 ++++++------ services/docker.go | 22 +++++----------- services/instance.go | 47 ++++++++++++++++++++-------------- services/session.go | 61 ++++++++++++++++++++++++++++++++++++-------- 6 files changed, 101 insertions(+), 54 deletions(-) diff --git a/Dockerfile.dind b/Dockerfile.dind index 34ead6c..6edd6ea 100644 --- a/Dockerfile.dind +++ b/Dockerfile.dind @@ -5,4 +5,5 @@ RUN apk add --no-cache git tmux py-pip apache2-utils vim \ COPY vimrc ./root/.vimrc +CMD docker daemon --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --storage-driver=vfs &>- & /bin/sh diff --git a/api.go b/api.go index d867ca5..1e2c7c7 100644 --- a/api.go +++ b/api.go @@ -3,6 +3,7 @@ package main import ( "log" "net/http" + "os" "github.com/franela/play-with-docker/handlers" "github.com/franela/play-with-docker/services" @@ -11,11 +12,17 @@ import ( ) func main() { + server := services.CreateWSServer() server.On("connection", handlers.WS) server.On("error", handlers.WSError) + err := services.LoadSessionsFromDisk() + if err != nil && !os.IsNotExist(err) { + log.Fatal("Error decoding sessions from disk ", err) + } + r := mux.NewRouter() r.StrictSlash(false) diff --git a/services/client.go b/services/client.go index fdfa3b2..e2550d9 100644 --- a/services/client.go +++ b/services/client.go @@ -12,7 +12,8 @@ type ViewPort struct { } type Client struct { - SO socketio.Socket + Id string + so socketio.Socket ViewPort ViewPort } @@ -24,7 +25,7 @@ func (c *Client) ResizeViewPort(cols, rows uint) { func NewClient(so socketio.Socket, session *Session) *Client { so.Join(session.Id) - c := &Client{SO: so} + c := &Client{so: so, Id: so.Id()} so.On("session close", func() { CloseSession(session) @@ -33,8 +34,8 @@ func NewClient(so socketio.Socket, session *Session) *Client { 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 instance != nil && len(data) > 0 { - instance.Conn.Conn.Write([]byte(data)) + if instance != nil && instance.conn != nil && len(data) > 0 { + instance.conn.Conn.Write([]byte(data)) } }) @@ -53,13 +54,13 @@ func NewClient(so socketio.Socket, session *Session) *Client { }) 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:]...) + for i, cl := range session.clients { + if cl.Id == c.Id { + session.clients = append(session.clients[:i], session.clients[i+1:]...) break } } - if len(session.Clients) > 0 { + if len(session.clients) > 0 { vp := session.GetSmallestViewPort() // Resize all terminals in the session wsServer.BroadcastTo(session.Id, "viewport resize", vp.Cols, vp.Rows) diff --git a/services/docker.go b/services/docker.go index ec74535..223de90 100644 --- a/services/docker.go +++ b/services/docker.go @@ -55,20 +55,10 @@ func DeleteNetwork(id string) error { return nil } -func CreateExecConnection(id string, ctx context.Context) (string, error) { - conf := types.ExecConfig{Tty: true, AttachStdin: true, AttachStderr: true, AttachStdout: true, Cmd: []string{"sh"}, DetachKeys: "ctrl-x,ctrl-x"} - resp, err := c.ContainerExecCreate(ctx, id, conf) - if err != nil { - return "", err - } - - return resp.ID, nil -} - -func AttachExecConnection(execId string, ctx context.Context) (*types.HijackedResponse, error) { - conf := types.ExecConfig{Tty: true, AttachStdin: true, AttachStderr: true, AttachStdout: true, DetachKeys: "ctrl-x,ctrl-x"} - conn, err := c.ContainerExecAttach(ctx, execId, conf) +func CreateAttachConnection(id string, ctx context.Context) (*types.HijackedResponse, error) { + conf := types.ContainerAttachOptions{true, true, true, true, "ctrl-x,ctrl-x", true} + conn, err := c.ContainerAttach(ctx, id, conf) if err != nil { return nil, err } @@ -76,8 +66,8 @@ func AttachExecConnection(execId string, ctx context.Context) (*types.HijackedRe return &conn, nil } -func ResizeExecConnection(execId string, ctx context.Context, cols, rows uint) error { - return c.ContainerExecResize(ctx, execId, types.ResizeOptions{Height: rows, Width: cols}) +func ResizeConnection(name string, cols, rows uint) error { + return c.ContainerResize(context.Background(), name, types.ResizeOptions{Height: rows, Width: cols}) } func CreateInstance(net string, dindImage string) (*Instance, error) { @@ -88,7 +78,7 @@ func CreateInstance(net string, dindImage string) (*Instance, error) { t := true h.Resources.OomKillDisable = &t - conf := &container.Config{Image: dindImage, Tty: true} + conf := &container.Config{Image: dindImage, Tty: true, OpenStdin: true, AttachStdin: true, AttachStdout: true, AttachStderr: true} container, err := c.ContainerCreate(context.Background(), conf, h, nil, "") if err != nil { diff --git a/services/instance.go b/services/instance.go index ccfca44..0bbe1c5 100644 --- a/services/instance.go +++ b/services/instance.go @@ -5,20 +5,31 @@ import ( "io" "log" "os" + "sync" "golang.org/x/text/encoding" "github.com/docker/docker/api/types" ) +var rw sync.Mutex + type Instance struct { - Session *Session `json:"-"` + session *Session `json:"-"` Name string `json:"name"` Hostname string `json:"hostname"` IP string `json:"ip"` - Conn *types.HijackedResponse `json:"-"` - ExecId string `json:"-"` - Ctx context.Context `json:"-"` + conn *types.HijackedResponse `json:"-"` + ctx context.Context `json:"-"` +} + +func (i *Instance) IsConnected() bool { + return i.conn != nil + +} + +func (i *Instance) SetSession(s *Session) { + i.session = s } var dindImage string @@ -43,14 +54,18 @@ func NewInstance(session *Session) (*Instance, error) { if err != nil { return nil, err } - instance.Session = session + instance.session = session if session.Instances == nil { session.Instances = make(map[string]*Instance) } session.Instances[instance.Name] = instance - go instance.Exec() + go instance.Attach() + + rw.Lock() + err = saveSessionsToDisk() + rw.Unlock() wsServer.BroadcastTo(session.Id, "new instance", instance.Name, instance.IP, instance.Hostname) @@ -62,28 +77,23 @@ type sessionWriter struct { } func (s *sessionWriter) Write(p []byte) (n int, err error) { - wsServer.BroadcastTo(s.instance.Session.Id, "terminal out", s.instance.Name, string(p)) + wsServer.BroadcastTo(s.instance.session.Id, "terminal out", s.instance.Name, string(p)) return len(p), nil } func (i *Instance) ResizeTerminal(cols, rows uint) error { - return ResizeExecConnection(i.ExecId, i.Ctx, cols, rows) + return ResizeConnection(i.Name, cols, rows) } -func (i *Instance) Exec() { - i.Ctx = context.Background() +func (i *Instance) Attach() { + i.ctx = context.Background() + conn, err := CreateAttachConnection(i.Name, i.ctx) - 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 + i.conn = conn go func() { encoder := encoding.Replacement.NewEncoder() @@ -92,10 +102,9 @@ func (i *Instance) Exec() { }() select { - case <-i.Ctx.Done(): + case <-i.ctx.Done(): } } - func GetInstance(session *Session, name string) *Instance { //TODO: Use redis return session.Instances[name] diff --git a/services/session.go b/services/session.go index d7ec84f..01f4d0c 100644 --- a/services/session.go +++ b/services/session.go @@ -1,8 +1,10 @@ package services import ( + "encoding/gob" "log" "math" + "os" "sync" "time" @@ -13,17 +15,25 @@ import ( var wsServer *socketio.Server type Session struct { - sync.Mutex + rw sync.Mutex Id string `json:"id"` Instances map[string]*Instance `json:"instances"` - Clients []*Client `json:"-"` + clients []*Client `json:"-"` +} + +func (s *Session) Lock() { + s.rw.Lock() +} + +func (s *Session) Unlock() { + s.rw.Unlock() } func (s *Session) GetSmallestViewPort() ViewPort { - minRows := s.Clients[0].ViewPort.Rows - minCols := s.Clients[0].ViewPort.Cols + minRows := s.clients[0].ViewPort.Rows + minCols := s.clients[0].ViewPort.Cols - for _, c := range s.Clients { + 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))) } @@ -32,7 +42,7 @@ func (s *Session) GetSmallestViewPort() ViewPort { } func (s *Session) AddNewClient(c *Client) { - s.Clients = append(s.Clients, c) + s.clients = append(s.clients, c) } var sessions map[string]*Session @@ -51,12 +61,12 @@ func CreateWSServer() *socketio.Server { } func CloseSession(s *Session) error { - s.Lock() - defer s.Unlock() + s.rw.Lock() + defer s.rw.Unlock() 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() + i.conn.Close() if err := DeleteContainer(i.Name); err != nil { log.Println(err) return err @@ -88,10 +98,39 @@ func NewSession() (*Session, error) { log.Println("ERROR NETWORKING") return nil, err } - return s, nil } func GetSession(sessionId string) *Session { - return sessions[sessionId] + s := sessions[sessionId] + if s != nil { + for _, instance := range s.Instances { + if !instance.IsConnected() { + instance.SetSession(s) + go instance.Attach() + } + } + + } + return s +} + +func LoadSessionsFromDisk() error { + file, err := os.Open("./sessions.gob") + if err == nil { + decoder := gob.NewDecoder(file) + err = decoder.Decode(&sessions) + } + file.Close() + return err +} + +func saveSessionsToDisk() error { + file, err := os.Create("./sessions.gob") + if err == nil { + encoder := gob.NewEncoder(file) + err = encoder.Encode(&sessions) + } + file.Close() + return err }