From 7e45881dc3a884a9f8bd1d0ff49ea5f93210b3fa Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 8 Jun 2019 13:00:37 +1000 Subject: [PATCH] a lot of change --- pkg/commands/container.go | 5 ++ pkg/commands/os.go | 61 +++++++++++++++++++ pkg/config/app_config.go | 26 +++++---- pkg/gui/containers_panel.go | 113 ++++++++++++++++++++++++++++-------- pkg/gui/keybindings.go | 7 +++ pkg/gui/services_panel.go | 6 +- pkg/gui/subprocess.go | 8 +-- pkg/gui/view_helpers.go | 3 + pkg/i18n/english.go | 4 ++ pkg/tasks/tasks.go | 19 ++++-- test/testmain/main.go | 17 ++++++ 11 files changed, 222 insertions(+), 47 deletions(-) create mode 100644 test/testmain/main.go diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 83a9ac03..8ccdf040 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -283,6 +283,11 @@ func (c *Container) GetDisplayCPUPerc() string { return utils.ColoredString(stats.CPUPerc, clr) } +// ProducingLogs tells us whether we should bother checking a container's logs +func (c *Container) ProducingLogs() bool { + return c.Container.State == "running" +} + // GetColor Container color func (c *Container) GetColor() color.Attribute { switch c.Container.State { diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 8c2dcca3..24976e88 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -8,6 +8,7 @@ import ( "path/filepath" "regexp" "strings" + "sync" "time" "github.com/go-errors/errors" @@ -305,3 +306,63 @@ func (c *OSCommand) GetLazydockerPath() string { func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd { return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command) } + +// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C +func (c *OSCommand) PipeCommands(commandStrings ...string) error { + + cmds := make([]*exec.Cmd, len(commandStrings)) + + for i, str := range commandStrings { + cmds[i] = c.ExecutableFromString(str) + } + + for i := 0; i < len(cmds)-1; i++ { + stdout, err := cmds[i].StdoutPipe() + if err != nil { + return err + } + + cmds[i+1].Stdin = stdout + } + + // keeping this here in case I adapt this code for some other purpose in the future + // cmds[len(cmds)-1].Stdout = os.Stdout + + finalErrors := []string{} + + wg := sync.WaitGroup{} + wg.Add(len(cmds)) + + for _, cmd := range cmds { + currentCmd := cmd + go func() { + stderr, err := currentCmd.StderrPipe() + if err != nil { + c.Log.Error(err) + } + + if err := currentCmd.Start(); err != nil { + c.Log.Error(err) + } + + if b, err := ioutil.ReadAll(stderr); err == nil { + if len(b) > 0 { + finalErrors = append(finalErrors, string(b)) + } + } + + if err := currentCmd.Wait(); err != nil { + c.Log.Error(err) + } + + wg.Done() + }() + } + + wg.Wait() + + if len(finalErrors) > 0 { + return errors.New(strings.Join(finalErrors, "\n")) + } + return nil +} diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 59de1aae..c77b187d 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -101,12 +101,14 @@ type GuiConfig struct { } type CommandTemplatesConfig struct { - RestartService string `yaml:"restartService,omitempty"` - DockerCompose string `yaml:"dockerCompose,omitempty"` - StopService string `yaml:"stopService,omitempty"` - ServiceLogs string `yaml:"serviceLogs,omitempty"` - ViewServiceLogs string `yaml:"viewServiceLogs,omitempty"` - RebuildService string `yaml:"rebuildService,omitempty"` + RestartService string `yaml:"restartService,omitempty"` + DockerCompose string `yaml:"dockerCompose,omitempty"` + StopService string `yaml:"stopService,omitempty"` + ServiceLogs string `yaml:"serviceLogs,omitempty"` + ViewServiceLogs string `yaml:"viewServiceLogs,omitempty"` + RebuildService string `yaml:"rebuildService,omitempty"` + ContainerLogs string `yaml:"containerLogs,omitempty"` + ContainerTTYLogs string `yaml:"containerTTYLogs,omitempty"` } type OSConfig struct { @@ -152,11 +154,13 @@ func GetDefaultConfig() UserConfig { Reporting: "undetermined", ConfirmOnQuit: false, CommandTemplates: CommandTemplatesConfig{ - RestartService: "docker-compose restart {{ .Name }}", - RebuildService: "docker-compose up -d --build {{ .Name }}", - DockerCompose: "apdev compose", - StopService: "apdev stop {{ .Name }}", - ServiceLogs: "apdev logs {{ .Name }}", + RestartService: "docker-compose restart {{ .Name }}", + RebuildService: "docker-compose up -d --build {{ .Name }}", + DockerCompose: "apdev compose", + StopService: "apdev stop {{ .Name }}", + ServiceLogs: "apdev logs {{ .Name }}", + ContainerLogs: "docker logs --timestamps --follow --tail=100 {{ .ID }}", + ContainerTTYLogs: "docker logs --follow --tail=100 {{ .ID }}", }, OS: GetPlatformDefaultConfig(), Update: UpdateConfig{ diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 4aae8e9c..34ad86da 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -2,6 +2,7 @@ package gui import ( "bufio" + "context" "encoding/json" "fmt" "os/exec" @@ -81,6 +82,8 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error { mainView := gui.getMainView() + gui.clearMainView() + switch gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] { case "logs": if err := gui.renderContainerLogs(mainView, container); err != nil { @@ -122,7 +125,7 @@ func (gui *Gui) renderContainerStats(mainView *gocui.View, container *commands.C mainView.Title = "Stats" mainView.Wrap = false - return gui.T.NewTickerTask(time.Second, func() { + return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) { width, _ := mainView.Size() contents, err := container.RenderStats(width) @@ -139,19 +142,52 @@ func (gui *Gui) renderContainerLogs(mainView *gocui.View, container *commands.Co mainView.Title = "Logs" if container.Details.Config.OpenStdin { - return gui.renderLogsForTTYContainer(mainView, container) + return gui.renderLogsForTTYContainer(container) } - return gui.renderLogsForRegularContainer(mainView, container) + return gui.renderLogsForRegularContainer(container) } -func (gui *Gui) renderLogsForRegularContainer(mainView *gocui.View, container *commands.Container) error { - var cmd *exec.Cmd - cmd = gui.OSCommand.RunCustomCommand("docker logs --timestamps --follow --since=2h " + container.ID) +func (gui *Gui) renderLogsForRegularContainer(container *commands.Container) error { + mainView := gui.getMainView() + go gui.T.NewTickerTask(time.Millisecond*200, nil, func(stop, notifyStopped chan struct{}) { - cmd.Stdout = mainView - cmd.Stderr = mainView + gui.clearMainView() - gui.runProcessWithLock(cmd) + cmd := gui.OSCommand.RunCustomCommand(utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.ContainerLogs, container)) + + cmd.Stdout = mainView + cmd.Stderr = mainView + + cmd.Start() + + go func() { + <-stop + cmd.Process.Kill() + return + }() + + cmd.Wait() + + // if we are here because the task has been stopped, we should return + // if we are here then the container must have exited, meaning we should wait until it's back again before + L: + for { + select { + case <-stop: + return + default: + result, err := gui.DockerCommand.Client.ContainerInspect(context.Background(), container.ID) + if err != nil { + gui.Log.Error(err) + return + } + if result.State.Running { + break L + } + time.Sleep(time.Millisecond * 100) + } + } + }) return nil } @@ -170,25 +206,42 @@ func (gui *Gui) runProcessWithLock(cmd *exec.Cmd) { }) } -func (gui *Gui) renderLogsForTTYContainer(mainView *gocui.View, container *commands.Container) error { - var cmd *exec.Cmd - cmd = gui.OSCommand.RunCustomCommand("docker logs --since=2h --follow " + container.ID) +func (gui *Gui) renderLogsForTTYContainer(container *commands.Container) error { + mainView := gui.getMainView() + gui.T.NewTickerTask(time.Millisecond*200, nil, func(stop, notifyStopped chan struct{}) { + gui.clearMainView() - r, err := cmd.StdoutPipe() - if err != nil { - return err - } + command := utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.ContainerTTYLogs, container) + gui.Log.Warn(command) + cmd := gui.OSCommand.RunCustomCommand(command) - go func() { - s := bufio.NewScanner(r) - s.Split(bufio.ScanLines) - for s.Scan() { - // I might put a check on the stopped channel here. Would mean more code duplication though - mainView.Write(append(s.Bytes(), '\n')) + // for some reason just saying cmd.Stdout = mainView does not work here as it does for non-tty containers, so we feed it through line by line + r, err := cmd.StdoutPipe() + if err != nil { + gui.ErrorChan <- err } - }() - gui.runProcessWithLock(cmd) + go func() { + s := bufio.NewScanner(r) + s.Split(bufio.ScanLines) + for s.Scan() { + // I might put a check on the stopped channel here. Would mean more code duplication though + mainView.Write(append(s.Bytes(), '\n')) + gui.Log.Warn(s.Text()) + } + }() + + cmd.Start() + + go func() { + <-stop + cmd.Process.Kill() + return + }() + + cmd.Wait() + }) + return nil } @@ -444,3 +497,15 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { gui.SubProcess = c return gui.Errors.ErrSubProcess } + +func (gui *Gui) handlePruneContainers(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error { + return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { + err := gui.DockerCommand.PruneContainers() + if err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + return nil + }) + }, nil) +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index f7fa50c4..f3552ade 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -193,6 +193,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleContainerAttach, Description: gui.Tr.Attach, }, + { + ViewName: "containers", + Key: 'D', + Modifier: gocui.ModNone, + Handler: gui.handlePruneContainers, + Description: gui.Tr.PruneContainers, + }, { ViewName: "services", Key: 'd', diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 9f87569a..44baabc7 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -291,11 +291,11 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { options := []*commandOption{ { description: gui.Tr.Restart, - command: utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.RestartService, service.Name), + command: utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.RestartService, service), }, { description: gui.Tr.Rebuild, - command: utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.RebuildService, service.Name), + command: utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.RebuildService, service), }, { description: gui.Tr.Cancel, @@ -323,7 +323,7 @@ func (gui *Gui) createCommandMenu(options []*commandOption, status string) error cmd.Run() - done <- struct{}{} + close(done) errorMessage := stderrBuf.String() if errorMessage != "" { diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index bd50a418..28f005d2 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -57,14 +57,14 @@ func (gui *Gui) runCommand() error { signal.Stop(c) + fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen)) + + fmt.Scanln() // wait for enter press + gui.SubProcess.Stdout = ioutil.Discard gui.SubProcess.Stderr = ioutil.Discard gui.SubProcess.Stdin = nil gui.SubProcess = nil - fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen)) - - fmt.Scanln() // wait for enter press - return nil } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 8b89a144..4e96b879 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -199,6 +199,9 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error { if err := v.SetOrigin(0, 0); err != nil { return err } + if err := v.SetCursor(0, 0); err != nil { + return err + } return gui.setViewContent(gui.g, v, s) }) return nil diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 6fa266b4..73ca5459 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -47,6 +47,8 @@ type TranslationSet struct { RemoveImage string RemoveWithoutPrune string PruneImages string + PruneContainers string + ConfirmPruneContainers string ConfirmPruneImages string PruningStatus string StopService string @@ -88,6 +90,7 @@ func englishSet() TranslationSet { ViewLogs: "view logs", RemoveImage: "remove image", RemoveWithoutPrune: "remove without deleting untagged parents", + PruneContainers: "prune exited containers", PruneImages: "prune unused images", ViewRestartOptions: "view restart options", @@ -108,6 +111,7 @@ func englishSet() TranslationSet { MustForceToRemoveContainer: "You cannot remove a running container unless you force it. Do you want to force it?", NotEnoughSpace: "Not enough space to render panels", ConfirmPruneImages: "Are you sure you want to prune all unused images?", + ConfirmPruneContainers: "Are you sure you want to prune all stopped containers?", StopService: "Are you sure you want to stop this service's containers? (enter/esc)", StopContainer: "Are you sure you want to stop this container?", PressEnterToReturn: "Press enter to return to lazydocker", diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go index 35203966..81abf8dc 100644 --- a/pkg/tasks/tasks.go +++ b/pkg/tasks/tasks.go @@ -43,29 +43,38 @@ func (t *TaskManager) NewTask(f func(stop chan struct{})) error { go func() { f(stop) - notifyStopped <- struct{}{} + close(notifyStopped) }() return nil } func (t *Task) Stop() { - t.stop <- struct{}{} + close(t.stop) <-t.notifyStopped return } // NewTickerTask is a convenience function for making a new task that repeats some action once per e.g. second -func (t *TaskManager) NewTickerTask(duration time.Duration, f func()) error { +// the before function gets called after the lock is obtained, but before the ticker starts. +func (t *TaskManager) NewTickerTask(duration time.Duration, before func(stop chan struct{}), f func(stop, notifyStopped chan struct{})) error { + notifyStopped := make(chan struct{}) + return t.NewTask(func(stop chan struct{}) { + if before != nil { + before(stop) + } tickChan := time.NewTicker(duration) - f() // calling f first so that we're not waiting for the first tick + // calling f first so that we're not waiting for the first tick + f(stop, notifyStopped) for { select { + case <-notifyStopped: + return case <-stop: return case <-tickChan.C: - f() + f(stop, notifyStopped) } } }) diff --git a/test/testmain/main.go b/test/testmain/main.go new file mode 100644 index 00000000..ecdf844e --- /dev/null +++ b/test/testmain/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "os" + "os/exec" + "strings" +) + +func main() { + cmd := exec.Command("docker", strings.Split("logs --follow 29754cb1ab9a", " ")...) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + cmd.Start() + cmd.Wait() +}