a lot of change

This commit is contained in:
Jesse Duffield 2019-06-08 13:00:37 +10:00
parent 07a01d2511
commit 7e45881dc3
11 changed files with 222 additions and 47 deletions

View file

@ -283,6 +283,11 @@ func (c *Container) GetDisplayCPUPerc() string {
return utils.ColoredString(stats.CPUPerc, clr) 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 // GetColor Container color
func (c *Container) GetColor() color.Attribute { func (c *Container) GetColor() color.Attribute {
switch c.Container.State { switch c.Container.State {

View file

@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"sync"
"time" "time"
"github.com/go-errors/errors" "github.com/go-errors/errors"
@ -305,3 +306,63 @@ func (c *OSCommand) GetLazydockerPath() string {
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd { func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command) 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
}

View file

@ -101,12 +101,14 @@ type GuiConfig struct {
} }
type CommandTemplatesConfig struct { type CommandTemplatesConfig struct {
RestartService string `yaml:"restartService,omitempty"` RestartService string `yaml:"restartService,omitempty"`
DockerCompose string `yaml:"dockerCompose,omitempty"` DockerCompose string `yaml:"dockerCompose,omitempty"`
StopService string `yaml:"stopService,omitempty"` StopService string `yaml:"stopService,omitempty"`
ServiceLogs string `yaml:"serviceLogs,omitempty"` ServiceLogs string `yaml:"serviceLogs,omitempty"`
ViewServiceLogs string `yaml:"viewServiceLogs,omitempty"` ViewServiceLogs string `yaml:"viewServiceLogs,omitempty"`
RebuildService string `yaml:"rebuildService,omitempty"` RebuildService string `yaml:"rebuildService,omitempty"`
ContainerLogs string `yaml:"containerLogs,omitempty"`
ContainerTTYLogs string `yaml:"containerTTYLogs,omitempty"`
} }
type OSConfig struct { type OSConfig struct {
@ -152,11 +154,13 @@ func GetDefaultConfig() UserConfig {
Reporting: "undetermined", Reporting: "undetermined",
ConfirmOnQuit: false, ConfirmOnQuit: false,
CommandTemplates: CommandTemplatesConfig{ CommandTemplates: CommandTemplatesConfig{
RestartService: "docker-compose restart {{ .Name }}", RestartService: "docker-compose restart {{ .Name }}",
RebuildService: "docker-compose up -d --build {{ .Name }}", RebuildService: "docker-compose up -d --build {{ .Name }}",
DockerCompose: "apdev compose", DockerCompose: "apdev compose",
StopService: "apdev stop {{ .Name }}", StopService: "apdev stop {{ .Name }}",
ServiceLogs: "apdev logs {{ .Name }}", ServiceLogs: "apdev logs {{ .Name }}",
ContainerLogs: "docker logs --timestamps --follow --tail=100 {{ .ID }}",
ContainerTTYLogs: "docker logs --follow --tail=100 {{ .ID }}",
}, },
OS: GetPlatformDefaultConfig(), OS: GetPlatformDefaultConfig(),
Update: UpdateConfig{ Update: UpdateConfig{

View file

@ -2,6 +2,7 @@ package gui
import ( import (
"bufio" "bufio"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os/exec" "os/exec"
@ -81,6 +82,8 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
mainView := gui.getMainView() mainView := gui.getMainView()
gui.clearMainView()
switch gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] { switch gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] {
case "logs": case "logs":
if err := gui.renderContainerLogs(mainView, container); err != nil { 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.Title = "Stats"
mainView.Wrap = false 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() width, _ := mainView.Size()
contents, err := container.RenderStats(width) contents, err := container.RenderStats(width)
@ -139,19 +142,52 @@ func (gui *Gui) renderContainerLogs(mainView *gocui.View, container *commands.Co
mainView.Title = "Logs" mainView.Title = "Logs"
if container.Details.Config.OpenStdin { 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 { func (gui *Gui) renderLogsForRegularContainer(container *commands.Container) error {
var cmd *exec.Cmd mainView := gui.getMainView()
cmd = gui.OSCommand.RunCustomCommand("docker logs --timestamps --follow --since=2h " + container.ID) go gui.T.NewTickerTask(time.Millisecond*200, nil, func(stop, notifyStopped chan struct{}) {
cmd.Stdout = mainView gui.clearMainView()
cmd.Stderr = mainView
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 return nil
} }
@ -170,25 +206,42 @@ func (gui *Gui) runProcessWithLock(cmd *exec.Cmd) {
}) })
} }
func (gui *Gui) renderLogsForTTYContainer(mainView *gocui.View, container *commands.Container) error { func (gui *Gui) renderLogsForTTYContainer(container *commands.Container) error {
var cmd *exec.Cmd mainView := gui.getMainView()
cmd = gui.OSCommand.RunCustomCommand("docker logs --since=2h --follow " + container.ID) gui.T.NewTickerTask(time.Millisecond*200, nil, func(stop, notifyStopped chan struct{}) {
gui.clearMainView()
r, err := cmd.StdoutPipe() command := utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.ContainerTTYLogs, container)
if err != nil { gui.Log.Warn(command)
return err cmd := gui.OSCommand.RunCustomCommand(command)
}
go func() { // 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
s := bufio.NewScanner(r) r, err := cmd.StdoutPipe()
s.Split(bufio.ScanLines) if err != nil {
for s.Scan() { gui.ErrorChan <- err
// I might put a check on the stopped channel here. Would mean more code duplication though
mainView.Write(append(s.Bytes(), '\n'))
} }
}()
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 return nil
} }
@ -444,3 +497,15 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
gui.SubProcess = c gui.SubProcess = c
return gui.Errors.ErrSubProcess 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)
}

View file

@ -193,6 +193,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainerAttach, Handler: gui.handleContainerAttach,
Description: gui.Tr.Attach, Description: gui.Tr.Attach,
}, },
{
ViewName: "containers",
Key: 'D',
Modifier: gocui.ModNone,
Handler: gui.handlePruneContainers,
Description: gui.Tr.PruneContainers,
},
{ {
ViewName: "services", ViewName: "services",
Key: 'd', Key: 'd',

View file

@ -291,11 +291,11 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
options := []*commandOption{ options := []*commandOption{
{ {
description: gui.Tr.Restart, 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, 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, description: gui.Tr.Cancel,
@ -323,7 +323,7 @@ func (gui *Gui) createCommandMenu(options []*commandOption, status string) error
cmd.Run() cmd.Run()
done <- struct{}{} close(done)
errorMessage := stderrBuf.String() errorMessage := stderrBuf.String()
if errorMessage != "" { if errorMessage != "" {

View file

@ -57,14 +57,14 @@ func (gui *Gui) runCommand() error {
signal.Stop(c) 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.Stdout = ioutil.Discard
gui.SubProcess.Stderr = ioutil.Discard gui.SubProcess.Stderr = ioutil.Discard
gui.SubProcess.Stdin = nil gui.SubProcess.Stdin = nil
gui.SubProcess = 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 return nil
} }

View file

@ -199,6 +199,9 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
if err := v.SetOrigin(0, 0); err != nil { if err := v.SetOrigin(0, 0); err != nil {
return err return err
} }
if err := v.SetCursor(0, 0); err != nil {
return err
}
return gui.setViewContent(gui.g, v, s) return gui.setViewContent(gui.g, v, s)
}) })
return nil return nil

View file

@ -47,6 +47,8 @@ type TranslationSet struct {
RemoveImage string RemoveImage string
RemoveWithoutPrune string RemoveWithoutPrune string
PruneImages string PruneImages string
PruneContainers string
ConfirmPruneContainers string
ConfirmPruneImages string ConfirmPruneImages string
PruningStatus string PruningStatus string
StopService string StopService string
@ -88,6 +90,7 @@ func englishSet() TranslationSet {
ViewLogs: "view logs", ViewLogs: "view logs",
RemoveImage: "remove image", RemoveImage: "remove image",
RemoveWithoutPrune: "remove without deleting untagged parents", RemoveWithoutPrune: "remove without deleting untagged parents",
PruneContainers: "prune exited containers",
PruneImages: "prune unused images", PruneImages: "prune unused images",
ViewRestartOptions: "view restart options", 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?", MustForceToRemoveContainer: "You cannot remove a running container unless you force it. Do you want to force it?",
NotEnoughSpace: "Not enough space to render panels", NotEnoughSpace: "Not enough space to render panels",
ConfirmPruneImages: "Are you sure you want to prune all unused images?", 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)", StopService: "Are you sure you want to stop this service's containers? (enter/esc)",
StopContainer: "Are you sure you want to stop this container?", StopContainer: "Are you sure you want to stop this container?",
PressEnterToReturn: "Press enter to return to lazydocker", PressEnterToReturn: "Press enter to return to lazydocker",

View file

@ -43,29 +43,38 @@ func (t *TaskManager) NewTask(f func(stop chan struct{})) error {
go func() { go func() {
f(stop) f(stop)
notifyStopped <- struct{}{} close(notifyStopped)
}() }()
return nil return nil
} }
func (t *Task) Stop() { func (t *Task) Stop() {
t.stop <- struct{}{} close(t.stop)
<-t.notifyStopped <-t.notifyStopped
return return
} }
// NewTickerTask is a convenience function for making a new task that repeats some action once per e.g. second // 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{}) { return t.NewTask(func(stop chan struct{}) {
if before != nil {
before(stop)
}
tickChan := time.NewTicker(duration) 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 { for {
select { select {
case <-notifyStopped:
return
case <-stop: case <-stop:
return return
case <-tickChan.C: case <-tickChan.C:
f() f(stop, notifyStopped)
} }
} }
}) })

17
test/testmain/main.go Normal file
View file

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