mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
a lot of change
This commit is contained in:
parent
07a01d2511
commit
7e45881dc3
11 changed files with 222 additions and 47 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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 != "" {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
17
test/testmain/main.go
Normal 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()
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue