From 50b4a72d1495ee21be67e0877a297574939c8970 Mon Sep 17 00:00:00 2001 From: Berger Eugene Date: Thu, 7 Dec 2023 23:50:43 +0200 Subject: [PATCH] Add support for foreground processes --- process-compose.override.yaml | 10 ++--- src/app/project_opts.go | 41 ++++++++++++++++++++ src/app/project_runner.go | 27 ++++++------- src/app/system_test.go | 33 +++++++++++++--- src/cmd/project_runner.go | 17 +++++++-- src/tui/log-operations.go | 31 +++++++++++---- src/tui/proc-starter.go | 71 +++++++++++++++++++++++++++++++++++ src/tui/proc-table.go | 22 +++++++---- src/tui/view.go | 54 ++++++++++++++++++++++++-- src/types/process.go | 8 ++++ src/types/project.go | 6 +-- 11 files changed, 268 insertions(+), 52 deletions(-) create mode 100644 src/app/project_opts.go create mode 100644 src/tui/proc-starter.go diff --git a/process-compose.override.yaml b/process-compose.override.yaml index 0605fbf..8a8c224 100644 --- a/process-compose.override.yaml +++ b/process-compose.override.yaml @@ -116,7 +116,6 @@ processes: liveness_probe: exec: command: '[ $(docker inspect -f {{ "{{.State.Running}}" }} nginx_test) = true ]' -# command: '[ $(docker inspect -f "{{.State.Running}}" nginx_test) = true ]' initial_delay_seconds: 5 period_seconds: 2 timeout_seconds: 5 @@ -161,7 +160,6 @@ processes: entrypoint: description: "run process with entrypoint and no command" - command: "echo you should not see ls" entrypoint: - ls - -lFa @@ -170,8 +168,6 @@ processes: - / vim: - description: "run process with entrypoint and no command" - command: "vim" -# entrypoint: -# - vim - disabled: true + description: "run a foreground process" + command: "vim process-compose.override.yaml" + is_foreground: true diff --git a/src/app/project_opts.go b/src/app/project_opts.go new file mode 100644 index 0000000..340ff67 --- /dev/null +++ b/src/app/project_opts.go @@ -0,0 +1,41 @@ +package app + +import "github.com/f1bonacc1/process-compose/src/types" + +type ProjectOpts struct { + project *types.Project + processesToRun []string + noDeps bool + mainProcess string + mainProcessArgs []string + isTuiOn bool +} + +func (p *ProjectOpts) WithProject(project *types.Project) *ProjectOpts { + p.project = project + return p +} + +func (p *ProjectOpts) WithProcessesToRun(processesToRun []string) *ProjectOpts { + p.processesToRun = processesToRun + return p +} +func (p *ProjectOpts) WithNoDeps(noDeps bool) *ProjectOpts { + p.noDeps = noDeps + return p +} + +func (p *ProjectOpts) WithMainProcess(mainProcess string) *ProjectOpts { + p.mainProcess = mainProcess + return p +} + +func (p *ProjectOpts) WithMainProcessArgs(mainProcessArgs []string) *ProjectOpts { + p.mainProcessArgs = mainProcessArgs + return p +} + +func (p *ProjectOpts) WithIsTuiOn(isTuiOn bool) *ProjectOpts { + p.isTuiOn = isTuiOn + return p +} diff --git a/src/app/project_runner.go b/src/app/project_runner.go index ba0c1a0..8b2f19e 100644 --- a/src/app/project_runner.go +++ b/src/app/project_runner.go @@ -29,6 +29,7 @@ type ProjectRunner struct { projectState *types.ProjectState mainProcess string mainProcessArgs []string + isTuiOn bool } func (p *ProjectRunner) GetLexicographicProcessNames() ([]string, error) { @@ -89,7 +90,7 @@ func (p *ProjectRunner) runProcess(config *types.ProcessConfig) { procState, _ := p.GetProcessState(config.ReplicaName) isMain := config.Name == p.mainProcess hasMain := p.mainProcess != "" - printLogs := !hasMain + printLogs := !hasMain && !p.isTuiOn extraArgs := []string{} if isMain { extraArgs = p.mainProcessArgs @@ -599,13 +600,6 @@ func (p *ProjectRunner) GetProjectState(checkMem bool) (*types.ProjectState, err return p.projectState, nil } -func NewProjectRunner( - project *types.Project, - processesToRun []string, - noDeps bool, - mainProcess string, - mainProcessArgs []string, -) (*ProjectRunner, error) { func getMemoryUsage() *types.MemoryState { var m runtime.MemStats runtime.ReadMemStats(&m) @@ -622,6 +616,8 @@ func bToMb(b uint64) uint64 { return b / 1024 / 1024 } +func NewProjectRunner(opts *ProjectOpts) (*ProjectRunner, error) { + hostname, err := os.Hostname() if err != nil { log.Err(err).Msg("Failed get hostname") @@ -635,11 +631,12 @@ func bToMb(b uint64) uint64 { username = current.Username } runner := &ProjectRunner{ - project: project, - mainProcess: mainProcess, - mainProcessArgs: mainProcessArgs, + project: opts.project, + mainProcess: opts.mainProcess, + mainProcessArgs: opts.mainProcessArgs, + isTuiOn: opts.isTuiOn, projectState: &types.ProjectState{ - FileNames: project.FileNames, + FileNames: opts.project.FileNames, StartTime: time.Now(), UserName: username, HostName: hostname, @@ -647,10 +644,10 @@ func bToMb(b uint64) uint64 { }, } - if noDeps { - err = runner.selectRunningProcessesNoDeps(processesToRun) + if opts.noDeps { + err = runner.selectRunningProcessesNoDeps(opts.processesToRun) } else { - err = runner.selectRunningProcesses(processesToRun) + err = runner.selectRunningProcesses(opts.processesToRun) } if err != nil { return nil, err diff --git a/src/app/system_test.go b/src/app/system_test.go index 4496f8d..1129f1a 100644 --- a/src/app/system_test.go +++ b/src/app/system_test.go @@ -28,7 +28,14 @@ func TestSystem_TestFixtures(t *testing.T) { t.Errorf(err.Error()) return } - runner, err := NewProjectRunner(project, []string{}, false, "", []string{}) + runner, err := NewProjectRunner(&ProjectOpts{ + project: project, + processesToRun: []string{}, + noDeps: false, + mainProcess: "", + mainProcessArgs: []string{}, + isTuiOn: false, + }) if err != nil { t.Errorf(err.Error()) return @@ -48,7 +55,11 @@ func TestSystem_TestComposeWithLog(t *testing.T) { t.Errorf(err.Error()) return } - runner, err := NewProjectRunner(project, []string{}, false, "", []string{}) + runner, err := NewProjectRunner(&ProjectOpts{ + project: project, + processesToRun: []string{}, + mainProcessArgs: []string{}, + }) if err != nil { t.Errorf(err.Error()) return @@ -81,7 +92,11 @@ func TestSystem_TestComposeChain(t *testing.T) { t.Errorf(err.Error()) return } - runner, err := NewProjectRunner(project, []string{}, false, "", []string{}) + runner, err := NewProjectRunner(&ProjectOpts{ + project: project, + processesToRun: []string{}, + mainProcessArgs: []string{}, + }) if err != nil { t.Errorf(err.Error()) return @@ -117,7 +132,11 @@ func TestSystem_TestComposeChainExit(t *testing.T) { t.Errorf(err.Error()) return } - runner, err := NewProjectRunner(project, []string{}, false, "", []string{}) + runner, err := NewProjectRunner(&ProjectOpts{ + project: project, + processesToRun: []string{}, + mainProcessArgs: []string{}, + }) if err != nil { t.Errorf(err.Error()) return @@ -162,7 +181,11 @@ func TestSystem_TestComposeScale(t *testing.T) { t.Errorf(err.Error()) return } - runner, err := NewProjectRunner(project, []string{}, false, "", []string{}) + runner, err := NewProjectRunner(&ProjectOpts{ + project: project, + processesToRun: []string{}, + mainProcessArgs: []string{}, + }) if err != nil { t.Errorf(err.Error()) return diff --git a/src/cmd/project_runner.go b/src/cmd/project_runner.go index 5c53745..6e25c68 100644 --- a/src/cmd/project_runner.go +++ b/src/cmd/project_runner.go @@ -23,7 +23,16 @@ func getProjectRunner(process []string, noDeps bool, mainProcess string, mainPro log.Fatal().Msg(err.Error()) } - runner, err := app.NewProjectRunner(project, process, noDeps, mainProcess, mainProcessArgs) + prjOpts := app.ProjectOpts{} + + runner, err := app.NewProjectRunner( + prjOpts.WithIsTuiOn(*pcFlags.Headless). + WithMainProcess(mainProcess). + WithMainProcessArgs(mainProcessArgs). + WithProject(project). + WithProcessesToRun(process). + WithNoDeps(noDeps), + ) if err != nil { fmt.Println(err) log.Fatal().Msg(err.Error()) @@ -63,10 +72,10 @@ func runHeadless(project *app.ProjectRunner) int { } func runTui(project *app.ProjectRunner) int { - setSignal(func() { + /*setSignal(func() { tui.Stop() - }) - defer quiet()() + })*/ + //defer quiet()() go startTui(project) exitCode := project.Run() tui.Stop() diff --git a/src/tui/log-operations.go b/src/tui/log-operations.go index f4d93ef..f227310 100644 --- a/src/tui/log-operations.go +++ b/src/tui/log-operations.go @@ -1,6 +1,7 @@ package tui import ( + "context" "fmt" "github.com/f1bonacc1/glippy" "github.com/gdamore/tcell/v2" @@ -37,12 +38,18 @@ func (pv *pcView) startFollowLog(name string) { pv.exitSearch() pv.logFollow = true pv.followLog(name) - go pv.updateLogs() + var ctx context.Context + ctx, pv.cancelLogFn = context.WithCancel(context.Background()) + go pv.updateLogs(ctx) pv.updateHelpTextView() } func (pv *pcView) stopFollowLog() { pv.logFollow = false + if pv.cancelLogFn != nil { + pv.cancelLogFn() + pv.cancelLogFn = nil + } pv.unFollowLog() pv.updateHelpTextView() } @@ -70,15 +77,23 @@ func (pv *pcView) unFollowLog() { pv.logsText.Flush() } -func (pv *pcView) updateLogs() { +func (pv *pcView) updateLogs(ctx context.Context) { + pv.appView.QueueUpdateDraw(func() { + pv.logsText.Flush() + }) for { - pv.appView.QueueUpdateDraw(func() { - pv.logsText.Flush() - }) - if !pv.logFollow { - break + select { + case <-ctx.Done(): + log.Debug().Msg("Logs monitoring canceled") + return + case <-time.After(300 * time.Millisecond): + pv.appView.QueueUpdateDraw(func() { + pv.logsText.Flush() + }) + //if !pv.logFollow { + // return + //} } - time.Sleep(300 * time.Millisecond) } } diff --git a/src/tui/proc-starter.go b/src/tui/proc-starter.go new file mode 100644 index 0000000..0881987 --- /dev/null +++ b/src/tui/proc-starter.go @@ -0,0 +1,71 @@ +package tui + +import ( + "context" + "fmt" + "github.com/f1bonacc1/process-compose/src/types" + "github.com/rs/zerolog/log" + "os" + "os/exec" + "os/signal" + "syscall" +) + +func (pv *pcView) startProcess() { + name := pv.getSelectedProcName() + info, err := pv.project.GetProcessInfo(name) + if err != nil { + pv.showError(err.Error()) + return + } + if info.IsForeground { + pv.runForeground(info) + return + } + err = pv.project.StartProcess(name) + if err != nil { + pv.showError(err.Error()) + } +} + +func (pv *pcView) runForeground(info *types.ProcessConfig) bool { + pv.halt() + defer pv.resume() + return pv.appView.Suspend(func() { + err := pv.execute(info) + if err != nil { + log.Err(err).Msgf("Command failed") + } + }) +} + +func (pv *pcView) execute(info *types.ProcessConfig) error { + ctx, cancel := context.WithCancel(context.Background()) + defer func() { + cancel() + clearScreen() + }() + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + go func(cancel context.CancelFunc) { + select { + case sig := <-sigChan: + log.Debug().Msgf("Command canceled with signal %#v", sig) + cancel() + case <-ctx.Done(): + log.Debug().Msgf("Foreground process context canceled") + } + }(cancel) + + cmd := exec.CommandContext(ctx, info.Executable, info.Args...) + log.Debug().Str("exec", info.Executable).Strs("args", info.Args).Msg("running start") + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + err := cmd.Run() + log.Debug().Str("exec", info.Executable).Strs("args", info.Args).Msg("running end") + + return err +} + +func clearScreen() { + fmt.Print("\033[H\033[2J") +} diff --git a/src/tui/proc-table.go b/src/tui/proc-table.go index 3ba711b..6da4365 100644 --- a/src/tui/proc-table.go +++ b/src/tui/proc-table.go @@ -1,6 +1,7 @@ package tui import ( + "context" "fmt" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" @@ -85,8 +86,7 @@ func (pv *pcView) createProcTable() *tview.Table { name := pv.getSelectedProcName() pv.project.StopProcess(name) case pv.shortcuts.ShortCutKeys[ActionProcessStart].key: - name := pv.getSelectedProcName() - pv.project.StartProcess(name) + pv.startProcess() case pv.shortcuts.ShortCutKeys[ActionProcessRestart].key: name := pv.getSelectedProcName() pv.project.RestartProcess(name) @@ -131,12 +131,20 @@ func (pv *pcView) createProcTable() *tview.Table { return table } -func (pv *pcView) updateTable() { +func (pv *pcView) updateTable(ctx context.Context) { + pv.appView.QueueUpdateDraw(func() { + pv.fillTableData() + }) for { - pv.appView.QueueUpdateDraw(func() { - pv.fillTableData() - }) - time.Sleep(pv.refreshRate) + select { + case <-ctx.Done(): + log.Debug().Msg("Table monitoring canceled") + return + case <-time.After(pv.refreshRate): + pv.appView.QueueUpdateDraw(func() { + pv.fillTableData() + }) + } } } diff --git a/src/tui/view.go b/src/tui/view.go index e562f87..0402770 100644 --- a/src/tui/view.go +++ b/src/tui/view.go @@ -1,14 +1,18 @@ package tui import ( + "context" "fmt" "github.com/f1bonacc1/process-compose/src/client" "github.com/f1bonacc1/process-compose/src/config" "github.com/f1bonacc1/process-compose/src/updater" "github.com/gdamore/tcell/v2" "github.com/rs/zerolog/log" + "os" + "os/signal" "strconv" "sync" + "syscall" "time" "github.com/f1bonacc1/process-compose/src/app" @@ -54,6 +58,9 @@ type pcView struct { stateSorter StateSorter procColumns map[ColumnID]string refreshRate time.Duration + cancelFn context.CancelFunc + cancelLogFn context.CancelFunc + cancelSigFn context.CancelFunc } func newPcView(project app.IProject) *pcView { @@ -370,10 +377,39 @@ func (pv *pcView) startMonitoring() { } time.Sleep(time.Second) } - }(pcClient) } +// Halt stop the application event loop. +func (pv *pcView) halt() { + if pv.cancelFn != nil { + pv.cancelFn() + pv.cancelFn = nil + } + if pv.cancelLogFn != nil { + pv.cancelLogFn() + pv.cancelLogFn = nil + } + if pv.cancelSigFn != nil { + pv.cancelSigFn() + pv.cancelSigFn = nil + } +} + +// Resume restarts the app event loop. +func (pv *pcView) resume() { + var ctxTbl context.Context + var ctxLog context.Context + var ctxSig context.Context + ctxTbl, pv.cancelFn = context.WithCancel(context.Background()) + ctxLog, pv.cancelLogFn = context.WithCancel(context.Background()) + ctxSig, pv.cancelSigFn = context.WithCancel(context.Background()) + + go pv.updateTable(ctxTbl) + go pv.updateLogs(ctxLog) + go setSignal(ctxSig) +} + func SetupTui(project app.IProject, options ...Option) { pv := newPcView(project) @@ -383,8 +419,7 @@ func SetupTui(project app.IProject, options ...Option) { } } - go pv.updateTable() - go pv.updateLogs() + pv.resume() if config.CheckForUpdates == "true" { go pv.runOnce() } @@ -395,6 +430,19 @@ func SetupTui(project app.IProject, options ...Option) { } } +func setSignal(ctx context.Context) { + cancelChan := make(chan os.Signal, 1) + signal.Notify(cancelChan, syscall.SIGTERM, os.Interrupt, syscall.SIGHUP) + select { + case sig := <-cancelChan: + log.Info().Msgf("Caught %v - Shutting down the running processes...", sig) + Stop() + os.Exit(1) + case <-ctx.Done(): + log.Debug().Msg("TUI Signal handler stopped") + } +} + func Stop() { if pcv != nil { pcv.handleShutDown() diff --git a/src/types/process.go b/src/types/process.go index fe69d84..7bf2132 100644 --- a/src/types/process.go +++ b/src/types/process.go @@ -32,6 +32,7 @@ type ProcessConfig struct { Extensions map[string]interface{} `yaml:",inline"` Description string `yaml:"description,omitempty"` Vars Vars `yaml:"vars"` + IsForeground bool `yaml:"is_foreground"` ReplicaNum int ReplicaName string Executable string @@ -57,6 +58,10 @@ func (p *ProcessConfig) CalculateReplicaName() string { return fmt.Sprintf("%s-%0*d", p.Name, myWidth, p.ReplicaNum) } +func (p *ProcessConfig) IsDeferred() bool { + return p.IsForeground || p.Disabled +} + func NewProcessState(proc *ProcessConfig) *ProcessState { state := &ProcessState{ Name: proc.ReplicaName, @@ -72,6 +77,8 @@ func NewProcessState(proc *ProcessConfig) *ProcessState { } if proc.Disabled { state.Status = ProcessStateDisabled + } else if proc.IsForeground { + state.Status = ProcessStateForeground } return state } @@ -108,6 +115,7 @@ const ( const ( ProcessStateDisabled = "Disabled" + ProcessStateForeground = "Foreground" ProcessStatePending = "Pending" ProcessStateRunning = "Running" ProcessStateLaunching = "Launching" diff --git a/src/types/project.go b/src/types/project.go index 8b9c1c6..3fe8c79 100644 --- a/src/types/project.go +++ b/src/types/project.go @@ -53,7 +53,7 @@ func (p *Project) GetProcesses(names ...string) ([]ProcessConfig, error) { processes := []ProcessConfig{} if len(names) == 0 { for _, proc := range p.Processes { - if proc.Disabled { + if proc.IsDeferred() { continue } processes = append(processes, proc) @@ -62,7 +62,7 @@ func (p *Project) GetProcesses(names ...string) ([]ProcessConfig, error) { } for _, name := range names { if proc, ok := p.Processes[name]; ok { - if proc.Disabled { + if proc.IsDeferred() { continue } processes = append(processes, proc) @@ -71,7 +71,7 @@ func (p *Project) GetProcesses(names ...string) ([]ProcessConfig, error) { for _, process := range p.Processes { if process.Name == name { found = true - if process.Disabled { + if process.IsDeferred() { continue } processes = append(processes, process)