diff --git a/pkg/commands/container.go b/pkg/commands/container.go index d59abfaf..7ded44e9 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -30,15 +30,10 @@ type Container struct { OSCommand *OSCommand Log *logrus.Entry Stats ContainerCliStat + StatHistory []ContainerCliStat Details Details } -type DetailsCollection struct { - InspectDetails Details - CLIStats ContainerCliStat - ClientStats ContainerStats -} - type Details struct { ID string `json:"Id"` Created time.Time `json:"Created"` @@ -251,11 +246,13 @@ func (c *Container) GetDisplayStrings(isFocused bool) []string { // GetDisplayCPUPerc colors the cpu percentage based on how extreme it is func (c *Container) GetDisplayCPUPerc() string { - if c.Stats.CPUPerc == "" { + stats := c.GetStats() + + if stats.CPUPerc == "" { return "" } - percentage, err := strconv.ParseFloat(strings.TrimSuffix(c.Stats.CPUPerc, "%"), 32) + percentage, err := strconv.ParseFloat(strings.TrimSuffix(stats.CPUPerc, "%"), 32) if err != nil { c.Log.Error(err) return "" @@ -270,7 +267,7 @@ func (c *Container) GetDisplayCPUPerc() string { clr = color.FgWhite } - return utils.ColoredString(c.Stats.CPUPerc, clr) + return utils.ColoredString(stats.CPUPerc, clr) } // GetColor Container color @@ -344,3 +341,11 @@ func (c *Container) Attach() (*exec.Cmd, error) { func (c *Container) Top() (types.ContainerProcessList, error) { return c.Client.ContainerTop(context.Background(), c.ID, []string{}) } + +// GetStats gets the container's most recent stats +func (c *Container) GetStats() ContainerCliStat { + if len(c.StatHistory) == 0 { + return ContainerCliStat{} + } + return c.StatHistory[len(c.StatHistory)-1] +} diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index b176c6ff..f6416df5 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -1,12 +1,15 @@ package commands import ( + "bufio" "context" "encoding/json" "fmt" "sort" "strings" + "sync" + "github.com/acarl005/stripansi" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" @@ -25,6 +28,11 @@ type DockerCommand struct { Client *client.Client InDockerComposeProject bool ErrorChan chan error + ContainerMutex sync.Mutex + ServiceMutex sync.Mutex + Services []*Service + Containers []*Container + Images []*Image } // NewDockerCommand it runs git commands @@ -45,50 +53,54 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localize }, nil } -// UpdateContainerStats takes a slice of containers and returns the same slice but with new stats added -// TODO: consider using this for everything stats-related -func (c *DockerCommand) UpdateContainerStats(containers []*Container) { - // TODO: consider using a stream rather than polling - command := `docker stats --all --no-trunc --no-stream --format '{{json .}}'` - output, err := c.OSCommand.RunCommandWithOutput(command) +// MonitorContainerStats monitors a stream of container stats and updates the containers as each new stats object is received +func (c *DockerCommand) MonitorContainerStats() { + + command := `docker stats --all --no-trunc --format '{{json .}}'` + cmd := c.OSCommand.RunCustomCommand(command) + + r, err := cmd.StdoutPipe() if err != nil { c.ErrorChan <- err return } - jsonStats := "[" + strings.Join( - strings.Split( - strings.Trim(output, "\n"), "\n", - ), ",", - ) + "]" + cmd.Start() - c.Log.Warn(jsonStats) - - var stats []ContainerCliStat - if err := json.Unmarshal([]byte(jsonStats), &stats); err != nil { - c.ErrorChan <- err - return - } - - for _, stat := range stats { - for _, container := range containers { - if container.ID == stat.ID { - container.Stats = stat + scanner := bufio.NewScanner(r) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + var stats ContainerCliStat + // need to strip ANSI codes because uses escape sequences to clear the screen with each refresh + cleanString := stripansi.Strip(scanner.Text()) + if err := json.Unmarshal([]byte(cleanString), &stats); err != nil { + c.ErrorChan <- err + return + } + c.ContainerMutex.Lock() + for _, container := range c.Containers { + if container.ID == stats.ID { + container.StatHistory = append(container.StatHistory, stats) } } + c.ContainerMutex.Unlock() } - c.Log.Warn("updated containers") + cmd.Wait() return } // GetContainersAndServices returns a slice of docker containers -func (c *DockerCommand) GetContainersAndServices(currentServices []*Service) ([]*Container, []*Service, error) { +func (c *DockerCommand) GetContainersAndServices() error { + c.ServiceMutex.Lock() + defer c.ServiceMutex.Unlock() + + currentServices := c.Services containers, err := c.GetContainers() if err != nil { - return nil, nil, err + return err } var services []*Service @@ -98,7 +110,7 @@ func (c *DockerCommand) GetContainersAndServices(currentServices []*Service) ([] } else { services, err = c.GetServices() if err != nil { - return nil, nil, err + return err } } @@ -124,11 +136,19 @@ func (c *DockerCommand) GetContainersAndServices(currentServices []*Service) ([] return services[i].Name < services[j].Name }) - return containers, services, nil + c.Containers = containers + c.Services = services + + return nil } // GetContainers gets the docker containers func (c *DockerCommand) GetContainers() ([]*Container, error) { + c.ContainerMutex.Lock() + defer c.ContainerMutex.Unlock() + + currentContainers := c.Containers + containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true}) if err != nil { return nil, err @@ -149,12 +169,16 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) { OSCommand: c.OSCommand, Log: c.Log, } + + // bring across old stats information + for _, currentContainer := range currentContainers { + if currentContainer.ID == container.ID { + ownContainers[i].StatHistory = currentContainer.StatHistory // might need to use pointers here in case we're deep copying everything + ownContainers[i].Details = currentContainer.Details + } + } } - c.UpdateContainerDetails(ownContainers) - - c.UpdateContainerStats(ownContainers) - return ownContainers, nil } @@ -191,7 +215,12 @@ func (c *DockerCommand) GetServices() ([]*Service, error) { // UpdateContainerDetails attaches the details returned from docker inspect to each of the containers // this contains a bit more info than what you get from the go-docker client -func (c *DockerCommand) UpdateContainerDetails(containers []*Container) error { +func (c *DockerCommand) UpdateContainerDetails() error { + c.ContainerMutex.Lock() + defer c.ContainerMutex.Unlock() + + containers := c.Containers + ids := make([]string, len(containers)) for i, container := range containers { ids[i] = container.ID diff --git a/pkg/commands/image.go b/pkg/commands/image.go index 37b16c84..cfbdba2d 100644 --- a/pkg/commands/image.go +++ b/pkg/commands/image.go @@ -4,7 +4,6 @@ import ( "context" "strings" - "github.com/davecgh/go-spew/spew" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/fatih/color" @@ -91,8 +90,6 @@ func (i *Image) RenderHistory() (string, error) { return "", err } - i.Log.Warn(spew.Sdump(history)) - layers := make([]*Layer, len(history)) for i, layer := range history { layers[i] = &Layer{layer} diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 9c8d4dbc..8c2dcca3 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -1,12 +1,14 @@ package commands import ( + "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "regexp" "strings" + "time" "github.com/go-errors/errors" @@ -58,9 +60,11 @@ func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) { // RunCommandWithOutput wrapper around commands returning their output and error func (c *OSCommand) RunCommandWithOutput(command string) (string, error) { - c.Log.WithField("command", command).Info("RunCommand") cmd := c.ExecutableFromString(command) - return sanitisedCommandOutput(cmd.CombinedOutput()) + before := time.Now() + output, err := sanitisedCommandOutput(cmd.CombinedOutput()) + c.Log.Warn(fmt.Sprintf("'%s': %s", command, time.Since(before))) + return output, err } // RunExecutableWithOutput runs an executable file and returns its output @@ -77,7 +81,7 @@ func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error { // ExecutableFromString takes a string like `git status` and returns an executable command for it func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd { splitCmd := str.ToArgv(commandStr) - c.Log.Info(splitCmd) + // c.Log.Info(splitCmd) return c.command(splitCmd[0], splitCmd[1:]...) } diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 83bbfe2c..cb6cd9d1 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -28,7 +28,7 @@ func (gui *Gui) getSelectedContainer(g *gocui.Gui) (*commands.Container, error) return &commands.Container{}, gui.Errors.ErrNoContainers } - return gui.State.Containers[selectedLine], nil + return gui.DockerCommand.Containers[selectedLine], nil } func (gui *Gui) handleContainersFocus(g *gocui.Gui, v *gocui.View) error { @@ -42,7 +42,7 @@ func (gui *Gui) handleContainersFocus(g *gocui.Gui, v *gocui.View) error { prevSelectedLine := gui.State.Panels.Containers.SelectedLine newSelectedLine := cy - oy - if newSelectedLine > len(gui.State.Containers)-1 || len(utils.Decolorise(gui.State.Containers[newSelectedLine].Name)) < cx { + if newSelectedLine > len(gui.DockerCommand.Containers)-1 || len(utils.Decolorise(gui.DockerCommand.Containers[newSelectedLine].Name)) < cx { return gui.handleContainerSelect(gui.g, v) } @@ -75,7 +75,7 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error { gui.State.Panels.Main.ObjectKey = key } - if err := gui.focusPoint(0, gui.State.Panels.Containers.SelectedLine, len(gui.State.Containers), v); err != nil { + if err := gui.focusPoint(0, gui.State.Panels.Containers.SelectedLine, len(gui.DockerCommand.Containers), v); err != nil { return err } @@ -109,7 +109,7 @@ func (gui *Gui) renderContainerConfig(mainView *gocui.View, container *commands. mainView.Autoscroll = false mainView.Title = "Config" - data, err := json.MarshalIndent(&container.Container, "", " ") + data, err := json.MarshalIndent(&container.Details, "", " ") if err != nil { return err } @@ -242,25 +242,25 @@ func (gui *Gui) refreshContainersAndServices() error { return err } - if len(gui.State.Containers) > 0 && gui.State.Panels.Containers.SelectedLine == -1 { + if len(gui.DockerCommand.Containers) > 0 && gui.State.Panels.Containers.SelectedLine == -1 { gui.State.Panels.Containers.SelectedLine = 0 } - if len(gui.State.Containers)-1 < gui.State.Panels.Containers.SelectedLine { - gui.State.Panels.Containers.SelectedLine = len(gui.State.Containers) - 1 + if len(gui.DockerCommand.Containers)-1 < gui.State.Panels.Containers.SelectedLine { + gui.State.Panels.Containers.SelectedLine = len(gui.DockerCommand.Containers) - 1 } // doing the exact same thing for services - if len(gui.State.Services) > 0 && gui.State.Panels.Services.SelectedLine == -1 { + if len(gui.DockerCommand.Services) > 0 && gui.State.Panels.Services.SelectedLine == -1 { gui.State.Panels.Services.SelectedLine = 0 } - if len(gui.State.Services)-1 < gui.State.Panels.Services.SelectedLine { - gui.State.Panels.Services.SelectedLine = len(gui.State.Services) - 1 + if len(gui.DockerCommand.Services)-1 < gui.State.Panels.Services.SelectedLine { + gui.State.Panels.Services.SelectedLine = len(gui.DockerCommand.Services) - 1 } gui.g.Update(func(g *gocui.Gui) error { containersView.Clear() isFocused := gui.g.CurrentView().Name() == "containers" - list, err := utils.RenderList(gui.State.Containers, utils.IsFocused(isFocused)) + list, err := utils.RenderList(gui.DockerCommand.Containers, utils.IsFocused(isFocused)) if err != nil { return err } @@ -279,7 +279,7 @@ func (gui *Gui) refreshContainersAndServices() error { servicesView := gui.getServicesView() servicesView.Clear() isFocused = gui.g.CurrentView().Name() == "services" - list, err = utils.RenderList(gui.State.Services, utils.IsFocused(isFocused)) + list, err = utils.RenderList(gui.DockerCommand.Services, utils.IsFocused(isFocused)) if err != nil { return err } @@ -295,15 +295,7 @@ func (gui *Gui) refreshContainersAndServices() error { } func (gui *Gui) refreshStateContainersAndServices() error { - containers, services, err := gui.DockerCommand.GetContainersAndServices(gui.State.Services) - if err != nil { - return err - } - - gui.State.Containers = containers - gui.State.Services = services - - return nil + return gui.DockerCommand.GetContainersAndServices() } func (gui *Gui) handleContainersNextLine(g *gocui.Gui, v *gocui.View) error { @@ -312,7 +304,7 @@ func (gui *Gui) handleContainersNextLine(g *gocui.Gui, v *gocui.View) error { } panelState := gui.State.Panels.Containers - gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Containers), false) + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Containers), false) return gui.handleContainerSelect(gui.g, v) } @@ -323,7 +315,7 @@ func (gui *Gui) handleContainersPrevLine(g *gocui.Gui, v *gocui.View) error { } panelState := gui.State.Panels.Containers - gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Containers), true) + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Containers), true) return gui.handleContainerSelect(gui.g, v) } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index d1a5d592..cbac814f 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -109,9 +109,6 @@ type panelStates struct { } type guiState struct { - Services []*commands.Service - Containers []*commands.Container - Images []*commands.Image MenuItemCount int // can't store the actual list because it's of interface{} type PreviousView string Platform commands.Platform @@ -126,7 +123,6 @@ type guiState struct { func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config *config.AppConfig, errorChan chan error) (*Gui, error) { initialState := guiState{ - Containers: make([]*commands.Container, 0), PreviousView: "services", Platform: *oSCommand.Platform, Panels: &panelStates{ @@ -430,8 +426,8 @@ func (gui *Gui) layout(g *gocui.Gui) error { } listViews := map[*gocui.View]listViewState{ - containersView: {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.State.Containers)}, - imagesView: {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.State.Images)}, + containersView: {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.DockerCommand.Containers)}, + imagesView: {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.DockerCommand.Images)}, } // menu view might not exist so we check to be safe @@ -501,6 +497,7 @@ func (gui *Gui) renderGlobalOptions() error { } func (gui *Gui) goEvery(interval time.Duration, function func() error) { + _ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change go func() { for range time.Tick(interval) { _ = function() @@ -536,9 +533,12 @@ func (gui *Gui) Run() error { gui.waitForIntro.Wait() gui.goEvery(time.Millisecond*50, gui.renderAppStatus) gui.goEvery(time.Millisecond*30, gui.reRenderMain) - gui.goEvery(time.Millisecond*500, gui.refreshContainersAndServices) + gui.goEvery(time.Millisecond*100, gui.refreshContainersAndServices) + gui.goEvery(time.Millisecond*1000, gui.DockerCommand.UpdateContainerDetails) }() + go gui.DockerCommand.MonitorContainerStats() + go func() { for err := range gui.ErrorChan { gui.createErrorPanel(gui.g, err.Error()) diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 7b1f9ba2..e7ecb387 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -25,7 +25,7 @@ func (gui *Gui) getSelectedImage(g *gocui.Gui) (*commands.Image, error) { return &commands.Image{}, gui.Errors.ErrNoImages } - return gui.State.Images[selectedLine], nil + return gui.DockerCommand.Images[selectedLine], nil } func (gui *Gui) handleImagesFocus(g *gocui.Gui, v *gocui.View) error { @@ -39,7 +39,7 @@ func (gui *Gui) handleImagesFocus(g *gocui.Gui, v *gocui.View) error { prevSelectedLine := gui.State.Panels.Images.SelectedLine newSelectedLine := cy - oy - if newSelectedLine > len(gui.State.Images)-1 || len(utils.Decolorise(gui.State.Images[newSelectedLine].Name)) < cx { + if newSelectedLine > len(gui.DockerCommand.Images)-1 || len(utils.Decolorise(gui.DockerCommand.Images[newSelectedLine].Name)) < cx { return gui.handleImageSelect(gui.g, v) } @@ -72,7 +72,7 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error { gui.State.Panels.Main.ObjectKey = key } - if err := gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.State.Images), v); err != nil { + if err := gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), v); err != nil { return err } @@ -129,18 +129,18 @@ func (gui *Gui) refreshImages() error { return err } - if len(gui.State.Images) > 0 && gui.State.Panels.Images.SelectedLine == -1 { + if len(gui.DockerCommand.Images) > 0 && gui.State.Panels.Images.SelectedLine == -1 { gui.State.Panels.Images.SelectedLine = 0 } - if len(gui.State.Images)-1 < gui.State.Panels.Images.SelectedLine { - gui.State.Panels.Images.SelectedLine = len(gui.State.Images) - 1 + if len(gui.DockerCommand.Images)-1 < gui.State.Panels.Images.SelectedLine { + gui.State.Panels.Images.SelectedLine = len(gui.DockerCommand.Images) - 1 } gui.g.Update(func(g *gocui.Gui) error { ImagesView.Clear() isFocused := gui.g.CurrentView().Name() == "Images" - list, err := utils.RenderList(gui.State.Images, utils.IsFocused(isFocused)) + list, err := utils.RenderList(gui.DockerCommand.Images, utils.IsFocused(isFocused)) if err != nil { return err } @@ -161,7 +161,7 @@ func (gui *Gui) refreshStateImages() error { return err } - gui.State.Images = Images + gui.DockerCommand.Images = Images return nil } @@ -172,7 +172,7 @@ func (gui *Gui) handleImagesNextLine(g *gocui.Gui, v *gocui.View) error { } panelState := gui.State.Panels.Images - gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Images), false) + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Images), false) return gui.handleImageSelect(gui.g, v) } @@ -183,7 +183,7 @@ func (gui *Gui) handleImagesPrevLine(g *gocui.Gui, v *gocui.View) error { } panelState := gui.State.Panels.Images - gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Images), true) + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Images), true) return gui.handleImageSelect(gui.g, v) } diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index ac89ec71..4ba62f01 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -23,7 +23,7 @@ func (gui *Gui) getSelectedService(g *gocui.Gui) (*commands.Service, error) { return &commands.Service{}, errors.New("no service selected") } - return gui.State.Services[selectedLine], nil + return gui.DockerCommand.Services[selectedLine], nil } func (gui *Gui) handleServicesFocus(g *gocui.Gui, v *gocui.View) error { @@ -37,7 +37,7 @@ func (gui *Gui) handleServicesFocus(g *gocui.Gui, v *gocui.View) error { prevSelectedLine := gui.State.Panels.Services.SelectedLine newSelectedLine := cy - oy - if newSelectedLine > len(gui.State.Services)-1 || len(utils.Decolorise(gui.State.Services[newSelectedLine].Name)) < cx { + if newSelectedLine > len(gui.DockerCommand.Services)-1 || len(utils.Decolorise(gui.DockerCommand.Services[newSelectedLine].Name)) < cx { return gui.handleServiceSelect(gui.g, v) } @@ -67,7 +67,7 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error { gui.State.Panels.Main.ObjectKey = key } - if err := gui.focusPoint(0, gui.State.Panels.Services.SelectedLine, len(gui.State.Services), v); err != nil { + if err := gui.focusPoint(0, gui.State.Panels.Services.SelectedLine, len(gui.DockerCommand.Services), v); err != nil { return err } @@ -137,7 +137,7 @@ func (gui *Gui) handleServicesNextLine(g *gocui.Gui, v *gocui.View) error { } panelState := gui.State.Panels.Services - gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Services), false) + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Services), false) return gui.handleServiceSelect(gui.g, v) } @@ -148,7 +148,7 @@ func (gui *Gui) handleServicesPrevLine(g *gocui.Gui, v *gocui.View) error { } panelState := gui.State.Panels.Services - gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Services), true) + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Services), true) return gui.handleServiceSelect(gui.g, v) } diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go index 639724eb..995c8223 100644 --- a/pkg/tasks/tasks.go +++ b/pkg/tasks/tasks.go @@ -24,15 +24,10 @@ func NewTaskManager(log *logrus.Entry) *TaskManager { } func (t *TaskManager) NewTask(f func(stop chan struct{})) error { - t.Log.Warn("new task") - t.waitingMutex.Lock() defer t.waitingMutex.Unlock() - t.Log.Warn("locked mutex") - if t.currentTask != nil { - t.Log.Warn("about to ask current task to stop") t.currentTask.Stop() } @@ -46,7 +41,6 @@ func (t *TaskManager) NewTask(f func(stop chan struct{})) error { } go func() { - t.Log.Warn("running new task") f(stop) notifyStopped <- struct{}{} }() @@ -57,6 +51,5 @@ func (t *TaskManager) NewTask(f func(stop chan struct{})) error { func (t *Task) Stop() { t.stop <- struct{}{} <-t.notifyStopped - t.Log.Warn("task successfully stopped") return }