diff --git a/pkg/app/app.go b/pkg/app/app.go index a920f44..09a4978 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -56,8 +56,7 @@ func NewApp(config *config.AppConfig) (*App, error) { } func (app *App) Run() error { - err := app.Gui.RunWithSubprocesses() - return err + return app.Gui.Run() } func (app *App) Close() error { diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 8978b99..e4309e7 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -492,8 +492,7 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } func (gui *Gui) handlePruneContainers() error { @@ -537,8 +536,7 @@ func (gui *Gui) containerExecShell(container *commands.Container) error { resolvedCommand := utils.ApplyTemplate("docker exec -it {{ .Container.ID }} /bin/sh -c 'eval $(grep ^$(id -un): /etc/passwd | cut -d : -f 7-)'", commandObject) // attach and return the subprocess error cmd := gui.OSCommand.ExecutableFromString(resolvedCommand) - gui.SubProcess = cmd - return gui.Errors.ErrSubProcess + return gui.runSubprocess(cmd) } func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 1a23b71..da03eed 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -53,8 +53,7 @@ func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, command // if we have a command for attaching, we attach and return the subprocess error if option.customCommand.Attach { cmd := gui.OSCommand.ExecutableFromString(option.command) - gui.SubProcess = cmd - return gui.Errors.ErrSubProcess + return gui.runSubprocess(cmd) } return gui.WithWaitingStatus(waitingStatus, func() error { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1771098..c664edb 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -2,20 +2,14 @@ package gui import ( "context" - "os/exec" "strings" "sync" "time" "github.com/docker/docker/api/types" - // "io" - // "io/ioutil" - "github.com/go-errors/errors" - // "strings" - throttle "github.com/boz/go-throttle" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazydocker/pkg/commands" @@ -31,7 +25,6 @@ var OverlappingEdges = false // SentinelErrors are the errors that have special meaning and need to be checked // by calling functions. The less of these, the better type SentinelErrors struct { - ErrSubProcess error ErrNoContainers error ErrNoImages error ErrNoVolumes error @@ -49,7 +42,6 @@ type SentinelErrors struct { // localising things in the code. func (gui *Gui) GenerateSentinelErrors() { gui.Errors = SentinelErrors{ - ErrSubProcess: errors.New(gui.Tr.RunningSubprocess), ErrNoContainers: errors.New(gui.Tr.NoContainers), ErrNoImages: errors.New(gui.Tr.NoImages), ErrNoVolumes: errors.New(gui.Tr.NoVolumes), @@ -62,7 +54,6 @@ type Gui struct { Log *logrus.Entry DockerCommand *commands.DockerCommand OSCommand *commands.OSCommand - SubProcess *exec.Cmd State guiState Config *config.AppConfig Tr *i18n.TranslationSet @@ -73,8 +64,20 @@ type Gui struct { ErrorChan chan error CyclableViews []string Views Views - + // returns true if our views have been created and assigned to gui.Views. + // Views are setup only once, upon application start. ViewsSetup bool + + // if we've suspended the gui (e.g. because we've switched to a subprocess) + // we typically want to pause some things that are running like background + // file refreshes + PauseBackgroundThreads bool + + Mutexes +} + +type Mutexes struct { + SubprocessMutex sync.Mutex } type servicePanelState struct { @@ -130,11 +133,6 @@ type guiState struct { SubProcessOutput string Stats map[string]commands.ContainerStats - // SessionIndex tells us how many times we've come back from a subprocess. - // We increment it each time we switch to a new subprocess - // Every time we go to a subprocess we need to close a few goroutines so this index is used for that purpose - SessionIndex int - ScreenMode WindowMaximisation } @@ -165,8 +163,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand }, Project: &projectState{ContextIndex: 0}, }, - SessionIndex: 0, - ViewStack: []string{}, + ViewStack: []string{}, } cyclableViews := []string{"project", "containers", "images", "volumes"} @@ -193,13 +190,6 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand return gui, nil } -func max(a, b int) int { - if a > b { - return a - } - return b -} - func (gui *Gui) renderGlobalOptions() error { return gui.renderOptionsMap(map[string]string{ "PgUp/PgDn": gui.Tr.Scroll, @@ -211,15 +201,15 @@ func (gui *Gui) renderGlobalOptions() error { } func (gui *Gui) goEvery(interval time.Duration, function func() error) { - currentSessionIndex := gui.State.SessionIndex _ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { - if gui.State.SessionIndex > currentSessionIndex { + if gui.PauseBackgroundThreads { return } + _ = function() } }() @@ -289,6 +279,9 @@ func (gui *Gui) Run() error { } err = g.MainLoop() + if err == gocui.ErrQuit { + return nil + } return err } @@ -411,8 +404,12 @@ func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) editFile(filename string) error { - _, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename)) - return err + cmd, err := gui.OSCommand.EditFile(filename) + if err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + + return gui.runSubprocess(cmd) } func (gui *Gui) openFile(filename string) error { @@ -422,28 +419,10 @@ func (gui *Gui) openFile(filename string) error { return nil } -// runSyncOrAsyncCommand takes the output of a command that may have returned -// either no error, an error, or a subprocess to execute, and if a subprocess -// needs to be set on the gui object, it does so, and then returns the error -// the bool returned tells us whether the calling code should continue -func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) { - if err != nil { - if err != gui.Errors.ErrSubProcess { - return false, gui.createErrorPanel(gui.g, err.Error()) - } - } - if sub != nil { - gui.SubProcess = sub - return false, gui.Errors.ErrSubProcess - } - return true, nil -} - func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error { return gui.createPromptPanel(g, v, gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error { command := gui.trimmedContent(v) - gui.SubProcess = gui.OSCommand.RunCustomCommand(command) - return gui.Errors.ErrSubProcess + return gui.runSubprocess(gui.OSCommand.RunCustomCommand(command)) }) } diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 743c682..3dd1417 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -224,6 +224,5 @@ func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index d290911..09a4daf 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -293,8 +293,7 @@ func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error { @@ -308,8 +307,7 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { @@ -366,8 +364,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), ), f: func() error { - gui.SubProcess = gui.OSCommand.RunCustomCommand(rebuildCommand) - return gui.Errors.ErrSubProcess + return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand)) }, }, { diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index 059f05e..6d114bc 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -4,48 +4,43 @@ import ( "fmt" "io/ioutil" "os" + "os/exec" "os/signal" "strings" "github.com/fatih/color" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazydocker/pkg/utils" ) -// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration -// if the error returned from a run is a ErrSubProcess, it runs the subprocess -// otherwise it handles the error, possibly by quitting the application -func (gui *Gui) RunWithSubprocesses() error { - for { - if err := gui.Run(); err != nil { - if err == gocui.ErrQuit { - break - } else if err == gui.Errors.ErrSubProcess { - // preparing the state for when we return - gui.pushView(gui.currentViewName()) - // giving goEvery goroutines time to finish - gui.State.SessionIndex++ - - if err := gui.runCommand(); err != nil { - return err - } - - // pop here so we don't stack up view names - gui.popView() - // ensuring we render e.g. the logs of the currently selected item upon return - gui.State.Panels.Main.ObjectKey = "" - } else { - return err - } - } +func (gui *Gui) runSubprocess(cmd *exec.Cmd) error { + gui.Mutexes.SubprocessMutex.Lock() + defer gui.Mutexes.SubprocessMutex.Unlock() + + if err := gui.g.Suspend(); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + + gui.PauseBackgroundThreads = true + + cmdErr := gui.runSubprocess(cmd) + + if err := gui.g.Resume(); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) } + + gui.PauseBackgroundThreads = false + + if cmdErr != nil { + return gui.createErrorPanel(gui.g, cmdErr.Error()) + } + return nil } -func (gui *Gui) runCommand() error { - gui.SubProcess.Stdout = os.Stdout - gui.SubProcess.Stderr = os.Stdout - gui.SubProcess.Stdin = os.Stdin +func (gui *Gui) runCommand(cmd *exec.Cmd) error { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + cmd.Stdin = os.Stdin stop := make(chan os.Signal, 1) defer signal.Stop(stop) @@ -54,23 +49,22 @@ func (gui *Gui) runCommand() error { signal.Notify(stop, os.Interrupt) <-stop - if err := gui.OSCommand.Kill(gui.SubProcess); err != nil { + if err := gui.OSCommand.Kill(cmd); err != nil { gui.Log.Error(err) } }() - fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(gui.SubProcess.Args, " "), color.FgBlue)) + fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(cmd.Args, " "), color.FgBlue)) - if err := gui.SubProcess.Run(); err != nil { + if err := cmd.Run(); err != nil { // not handling the error explicitly because usually we're going to see it // in the output anyway gui.Log.Error(err) } - gui.SubProcess.Stdin = nil - gui.SubProcess.Stdout = ioutil.Discard - gui.SubProcess.Stderr = ioutil.Discard - gui.SubProcess = nil + cmd.Stdin = nil + cmd.Stdout = ioutil.Discard + cmd.Stderr = ioutil.Discard gui.promptToReturn() diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 90a75f3..1532eee 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -8,7 +8,6 @@ func dutchSet() TranslationSet { StoppingStatus: "stoppen", RunningCustomCommandStatus: "Aangepast commando draaien", - RunningSubprocess: "subprocess draaien", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", ErrorOccurred: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 62d9a1c..9f225c4 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -13,7 +13,6 @@ type TranslationSet struct { Scroll string Close string ErrorTitle string - RunningSubprocess string NoViewMachingNewLineFocusedSwitchStatement string OpenConfig string EditConfig string @@ -121,7 +120,6 @@ func englishSet() TranslationSet { RunningCustomCommandStatus: "running custom command", RunningBulkCommandStatus: "running bulk command", - RunningSubprocess: "running subprocess", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", ErrorOccurred: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/german.go b/pkg/i18n/german.go index aba4f73..4a205aa 100644 --- a/pkg/i18n/german.go +++ b/pkg/i18n/german.go @@ -8,7 +8,6 @@ func germanSet() TranslationSet { StoppingStatus: "anhalten", RunningCustomCommandStatus: "führt benutzerdefinierten Befehl aus", - RunningSubprocess: "Unterprozess ausführen", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", ErrorOccurred: "Es ist ein Fehler aufgetreten! Bitte erstelle ein Issue hier: https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 2525d63..2cf12dd 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -8,7 +8,6 @@ func polishSet() TranslationSet { StoppingStatus: "zatrzymywanie", RunningCustomCommandStatus: "uruchamianie własnej komendty", - RunningSubprocess: "uruchamianie podprocesu", NoViewMachingNewLineFocusedSwitchStatement: "Żaden widok nie odpowiada instrukcji przełączenia newLineFocused", ErrorOccurred: "Wystąpił błąd! Proszę go zgłosić na https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/turkish.go b/pkg/i18n/turkish.go index 9b51af2..4a378b7 100644 --- a/pkg/i18n/turkish.go +++ b/pkg/i18n/turkish.go @@ -8,7 +8,6 @@ func turkishSet() TranslationSet { StoppingStatus: "durduruluyor", RunningCustomCommandStatus: "özel komut çalıştır", - RunningSubprocess: "İkincil işlem yürütülüyor", NoViewMachingNewLineFocusedSwitchStatement: "NewLineFocused anahtar deyimi ile eşleşen görünüm yok", ErrorOccurred: "Bir hata oluştu! Lütfen https://github.com/jesseduffield/lazydocker/issues adresinden bir hataya ilişkin konu oluşturun",