no more separate code paths for tty container logs

This commit is contained in:
Jesse Duffield 2019-06-23 08:11:16 +10:00
parent 00f05ae012
commit 014dce909c
8 changed files with 61 additions and 52 deletions

View file

@ -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 != ""
}

View file

@ -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{

View file

@ -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()

View file

@ -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 {

View file

@ -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 {

View file

@ -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
})
},
},

View file

@ -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 {

View file

@ -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",