diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 7c383580..c8e03fc8 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -426,3 +426,8 @@ func (c *Container) RenderTop() (string, error) { return utils.RenderTable(append([][]string{result.Titles}, result.Processes...)) } + +// DetailsLoaded tells us whether we have yet loaded the details for a container. Because this is an asynchronous operation, sometimes we have the container before we have its details. Details is a struct, not a pointer to a struct, so it starts off with heaps of zero values. One of which is the container Image, which starts as a blank string. Given that every container should have an image, this is a good proxy to use +func (c *Container) DetailsLoaded() bool { + return c.Details.Image != "" +} diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 254569e1..67553675 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -109,6 +109,7 @@ type CommandTemplatesConfig struct { ServiceLogs string `yaml:"serviceLogs,omitempty"` ViewServiceLogs string `yaml:"viewServiceLogs,omitempty"` RebuildService string `yaml:"rebuildService,omitempty"` + RecreateService string `yaml:"recreateService,omitempty"` // ViewContainerLogs is for viewing the container logs in a subprocess. We have this as a separate command in case you want to show all the logs rather than just tail them for the sake of reducing CPU load when in the lazydocker GUI ViewContainerLogs string `yaml:"viewContainerLogs,omitempty"` @@ -179,6 +180,7 @@ func GetDefaultConfig() UserConfig { DockerCompose: "docker-compose", RestartService: "{{ .DockerCompose }} restart {{ .Service.Name }}", RebuildService: "{{ .DockerCompose }} up -d --build {{ .Service.Name }}", + RecreateService: "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}", StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}", ServiceLogs: "{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}", ViewServiceLogs: "{{ .DockerCompose }} logs --follow {{ .Service.Name }}", @@ -188,7 +190,6 @@ func GetDefaultConfig() UserConfig { CheckDockerComposeConfig: "{{ .DockerCompose }} config --quiet", ContainerLogs: "docker logs --timestamps --follow --since=60m {{ .Container.ID }}", ViewContainerLogs: "docker logs --timestamps --follow --since=60m {{ .Container.ID }}", - ContainerTTYLogs: "docker logs --follow --since=60m {{ .Container.ID }}", ServiceTop: "{{ .DockerCompose }} top {{ .Service.Name }}", }, CustomCommands: CustomCommands{ diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 6fda5f27..6366d5da 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -1,10 +1,8 @@ package gui import ( - "bufio" "encoding/json" "fmt" - "os/exec" "time" "github.com/docker/docker/api/types" @@ -164,51 +162,18 @@ func (gui *Gui) renderContainerLogs(container *commands.Container) error { mainView.Autoscroll = true mainView.Wrap = true - if container.Details.Config.OpenStdin { - return gui.renderLogsForTTYContainer(container) - } - return gui.renderLogsForRegularContainer(container) -} - -func (gui *Gui) renderLogsForRegularContainer(container *commands.Container) error { - return gui.renderLogs(container, gui.Config.UserConfig.CommandTemplates.ContainerLogs, func(cmd *exec.Cmd) { - mainView := gui.getMainView() - cmd.Stdout = mainView - cmd.Stderr = mainView - }) -} - -func (gui *Gui) renderLogsForTTYContainer(container *commands.Container) error { - return gui.renderLogs(container, gui.Config.UserConfig.CommandTemplates.ContainerTTYLogs, func(cmd *exec.Cmd) { - // 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 - } - - go func() { - mainView := gui.getMainView() - 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')) - } - }() - }) -} - -func (gui *Gui) renderLogs(container *commands.Container, template string, setup func(*exec.Cmd)) error { return gui.T.NewTickerTask(time.Millisecond*200, nil, func(stop, notifyStopped chan struct{}) { gui.clearMainView() command := utils.ApplyTemplate( - template, + gui.Config.UserConfig.CommandTemplates.ContainerLogs, gui.DockerCommand.NewCommandObject(commands.CommandObject{Container: container}), ) cmd := gui.OSCommand.RunCustomCommand(command) - setup(cmd) + mainView := gui.getMainView() + cmd.Stdout = mainView + cmd.Stderr = mainView cmd.Start() diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 9b7bddd9..67d4a2d9 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -1,6 +1,7 @@ package gui import ( + "strings" "sync" // "io" @@ -267,12 +268,17 @@ func (gui *Gui) Run() error { gui.goEvery(time.Millisecond*100, gui.refreshContainersAndServices) gui.goEvery(time.Millisecond*100, gui.refreshVolumes) gui.goEvery(time.Millisecond*1000, gui.DockerCommand.UpdateContainerDetails) + gui.goEvery(time.Millisecond*1000, gui.checkForContextChange) }() go gui.DockerCommand.MonitorContainerStats() go func() { for err := range gui.ErrorChan { + if strings.Contains(err.Error(), "No such container") { + // this happens all the time when e.g. restarting containers so we won't worry about it + gui.Log.Warn(err) + } gui.createErrorPanel(gui.g, err.Error()) } }() @@ -287,6 +293,11 @@ func (gui *Gui) Run() error { return err } +// checkForContextChange runs the currently focused panel's 'select' function, simulating the current item having just been selected. This will then trigger a check to see if anything's changed (e.g. a service has a new container) and if so, the appropriate code will run. For example, if you're reading logs from a service and all of a sudden its container changes, this will trigger the 'select' function, which will work out that the context is not different because of the new container, and then it will re-attempt to get the logs, this time for the correct container. This 'context' is stored in the main panel's ObjectKey. I'm using the term 'context' here more broadly than just the different tabs you can view in a panel. +func (gui *Gui) checkForContextChange() error { + return gui.newLineFocused(gui.g.CurrentView()) +} + func (gui *Gui) reRenderMain() error { mainView := gui.getMainView() if mainView == nil { diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index fd532287..7c4968c4 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -162,6 +162,9 @@ func (gui *Gui) layout(g *gocui.Gui) error { } v.Wrap = true v.FgColor = gocui.ColorWhite + + // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. + v.IgnoreCarriageReturns = true } if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil { diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index e7cc5361..91c0450c 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -64,7 +64,11 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error { return nil } - key := "services-" + service.ID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] + containerID := "" + if service.Container != nil { + containerID = service.Container.ID + } + key := "services-" + service.ID + "-" + containerID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] if gui.State.Panels.Main.ObjectKey == key { return nil } else { @@ -305,7 +309,12 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { rebuildCommand := utils.ApplyTemplate( gui.Config.UserConfig.CommandTemplates.RebuildService, - service, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), + ) + + recreateCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.RecreateService, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), ) options := []*commandOption{ @@ -320,8 +329,21 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { if err := service.Restart(); err != nil { return gui.createErrorPanel(gui.g, err.Error()) } - - return gui.refreshContainersAndServices() + return nil + }) + }, + }, { + description: gui.Tr.Recreate, + command: utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.RecreateService, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), + ), + f: func() error { + return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { + if err := gui.OSCommand.RunCommand(recreateCommand); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + return nil }) }, }, diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index b93db426..822e63d3 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -77,20 +77,20 @@ func (gui *Gui) resetMainView() { gui.getMainView().Wrap = true } -func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) newLineFocused(v *gocui.View) error { switch v.Name() { case "menu": - return gui.handleMenuSelect(g, v) + return gui.handleMenuSelect(gui.g, v) case "status": - return gui.handleStatusSelect(g, v) + return gui.handleStatusSelect(gui.g, v) case "services": - return gui.handleServiceSelect(g, v) + return gui.handleServiceSelect(gui.g, v) case "containers": - return gui.handleContainerSelect(g, v) + return gui.handleContainerSelect(gui.g, v) case "images": - return gui.handleImageSelect(g, v) + return gui.handleImageSelect(gui.g, v) case "volumes": - return gui.handleVolumeSelect(g, v) + return gui.handleVolumeSelect(gui.g, v) case "confirmation": return nil case "main": @@ -137,7 +137,7 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { return err } - return gui.newLineFocused(g, newView) + return gui.newLineFocused(newView) } func (gui *Gui) resetOrigin(v *gocui.View) error { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 833d8577..1db64eff 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -38,6 +38,7 @@ type TranslationSet struct { Stop string Restart string Rebuild string + Recreate string PreviousContext string NextContext string Attach string @@ -105,6 +106,7 @@ func englishSet() TranslationSet { Stop: "stop", Restart: "restart", Rebuild: "rebuild", + Recreate: "recreate", PreviousContext: "previous tab", NextContext: "next tab", Attach: "attach",