From 63c0e61eb4e0bad7672da928484c763fee9c032a Mon Sep 17 00:00:00 2001 From: Berger Eugene Date: Mon, 2 May 2022 01:49:19 +0300 Subject: [PATCH] TUI Support --- Makefile | 8 +- go.mod | 8 ++ go.sum | 17 +++ src/app/config.go | 11 +- src/app/process.go | 31 +++++- src/app/process_test.go | 67 ++++++++++++ src/app/project.go | 53 ++++++++-- src/main.go | 30 +++++- src/pclog/process_log_buffer.go | 50 +++++++++ src/tui/view.go | 177 ++++++++++++++++++++++++++++++++ 10 files changed, 436 insertions(+), 16 deletions(-) create mode 100644 src/app/process_test.go create mode 100644 src/pclog/process_log_buffer.go create mode 100644 src/tui/view.go diff --git a/Makefile b/Makefile index 4c06547..7f11049 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ ifeq ($(OS),Windows_NT) RM = cmd /C del /Q /F endif -.PHONY: test run +.PHONY: test run testrace buildrun: build run @@ -27,12 +27,16 @@ compile: test: go test -cover ./src/... + +testrace: + go test -race ./src/... + coverhtml: go test -coverprofile=coverage.out ./src go tool cover -html=coverage.out run: - ./bin/${BINARY_NAME}${EXT} + PC_DEBUG_MODE=1 ./bin/${BINARY_NAME}${EXT} clean: $(RM) bin/${BINARY_NAME}* diff --git a/go.mod b/go.mod index 97669fb..2f47886 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,17 @@ go 1.18 require ( github.com/fatih/color v1.13.0 + github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 github.com/gin-gonic/gin v1.7.7 github.com/joho/godotenv v1.4.0 + github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 github.com/swaggo/swag v1.8.1 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect + github.com/gdamore/encoding v1.0.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect @@ -24,12 +27,17 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.9 // indirect github.com/leodido/go-urn v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/ugorji/go/codec v1.1.7 // indirect golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e // indirect golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.10 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index ba5b3e4..8399e85 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM= +github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k= github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc= @@ -62,6 +66,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -72,6 +78,8 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= @@ -87,6 +95,10 @@ github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc= +github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= @@ -134,6 +146,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -143,11 +156,15 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= diff --git a/src/app/config.go b/src/app/config.go index 7cf02d6..0b8f157 100644 --- a/src/app/config.go +++ b/src/app/config.go @@ -15,6 +15,7 @@ type Project struct { runningProcesses map[string]*Process processStates map[string]*ProcessState + processLogs map[string]*pclog.ProcessLogBuffer mapMutex sync.Mutex logger pclog.PcLogger wg sync.WaitGroup @@ -33,10 +34,12 @@ type ProcessConfig struct { } type ProcessState struct { - Name string `json:"name"` - Status string `json:"status"` - Restarts int `json:"restarts"` - ExitCode int `json:"exit_code"` + Name string `json:"name"` + Status string `json:"status"` + SystemTime string `json:"system_time"` + Restarts int `json:"restarts"` + ExitCode int `json:"exit_code"` + Pid int `json:"pid"` } func (p ProcessConfig) GetDependencies() []string { diff --git a/src/app/process.go b/src/app/process.go index 1a8ed08..2e31122 100644 --- a/src/app/process.go +++ b/src/app/process.go @@ -26,10 +26,12 @@ type Process struct { procColor func(a ...interface{}) string noColor func(a ...interface{}) string redColor func(a ...interface{}) string + logBuffer *pclog.ProcessLogBuffer logger pclog.PcLogger cmd *exec.Cmd done bool replica int + startTime time.Time } func NewProcess( @@ -37,6 +39,7 @@ func NewProcess( logger pclog.PcLogger, procConf ProcessConfig, procState *ProcessState, + procLog *pclog.ProcessLogBuffer, replica int) *Process { colNumeric := rand.Intn(int(color.FgHiWhite)-int(color.FgHiBlack)) + int(color.FgHiBlack) //logger, _ := zap.NewProduction() @@ -51,6 +54,7 @@ func NewProcess( procState: procState, done: false, replica: replica, + logBuffer: procLog, } proc.procCond = *sync.NewCond(proc) return proc @@ -65,7 +69,9 @@ func (p *Process) Run() error { go p.handleOutput(stdout, p.handleInfo) go p.handleOutput(stderr, p.handleError) p.cmd.Start() + p.startTime = time.Now() p.procState.Status = ProcessStateRunning + p.procState.Pid = p.cmd.Process.Pid p.cmd.Wait() p.Lock() @@ -154,6 +160,7 @@ func (p *Process) onProcessEnd() { p.logger.Close() } p.procState.Status = ProcessStateCompleted + p.Lock() p.done = true p.Unlock() @@ -172,6 +179,25 @@ func (p *Process) getCommand() string { return p.procConf.Command } +func (p *Process) updateProcState() { + if p.procState.Status == ProcessStateRunning { + dur := time.Since(p.startTime) + p.procState.SystemTime = durationToString(dur) + } +} + +func durationToString(dur time.Duration) string { + if dur.Minutes() < 3 { + return dur.Round(time.Second).String() + } else if dur.Minutes() < 60 { + return fmt.Sprintf("%.0fm", dur.Minutes()) + } else if dur.Hours() < 24 { + return fmt.Sprintf("%dh%dm", int(dur.Hours()), int(dur.Minutes())%60) + } else { + return fmt.Sprintf("%dh", int(dur.Hours())) + } +} + func (p *Process) handleOutput(pipe io.ReadCloser, handler func(message string)) { @@ -184,12 +210,15 @@ func (p *Process) handleOutput(pipe io.ReadCloser, func (p *Process) handleInfo(message string) { p.logger.Info(message, p.GetName(), p.replica) - fmt.Printf("[%s]\t%s\n", p.procColor(p.GetNameWithReplica()), p.noColor(message)) + fmt.Printf("[%s]\t%s\n", p.procColor(p.GetNameWithReplica()), message) + p.logBuffer.Write(message) } func (p *Process) handleError(message string) { p.logger.Error(message, p.GetName(), p.replica) fmt.Printf("[%s]\t%s\n", p.procColor(p.GetNameWithReplica()), p.redColor(message)) + p.logBuffer.Write(fmt.Sprintf("[deeppink]%s[-:-:-]", message)) + } func getRunnerShell() string { diff --git a/src/app/process_test.go b/src/app/process_test.go new file mode 100644 index 0000000..7230d1e --- /dev/null +++ b/src/app/process_test.go @@ -0,0 +1,67 @@ +package app + +import ( + "testing" + "time" +) + +func TestDurationToString(t *testing.T) { + type args struct { + dur time.Duration + } + tests := []struct { + name string + args args + want string + }{ + { + name: "milis", + args: args{ + dur: 20 * time.Millisecond, + }, + want: "0s", + }, + { + name: "under 1m", + args: args{ + dur: 20 * time.Second, + }, + want: "20s", + }, + { + name: "under 3m", + args: args{ + dur: 150 * time.Second, + }, + want: "2m30s", + }, + { + name: "under 1h", + args: args{ + dur: 30 * time.Minute, + }, + want: "30m", + }, + { + name: "under 24h", + args: args{ + dur: 280 * time.Minute, + }, + want: "4h40m", + }, + { + name: "above 24h", + args: args{ + dur: 25*time.Hour + 50*time.Minute, + }, + want: "25h", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := durationToString(tt.args.dur); got != tt.want { + t.Errorf("DurationToString() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/src/app/project.go b/src/app/project.go index abc2c49..96ab7ab 100644 --- a/src/app/project.go +++ b/src/app/project.go @@ -20,6 +20,7 @@ var PROJ *Project func (p *Project) Run() { p.initProcessStates() + p.initProcessLogs() p.runningProcesses = make(map[string]*Process) runOrder := []ProcessConfig{} p.WithProcesses([]string{}, func(process ProcessConfig) error { @@ -35,6 +36,7 @@ func (p *Project) Run() { p.logger = pclog.NewLogger(p.LogLocation) defer p.logger.Close() } + //zerolog.SetGlobalLevel(zerolog.PanicLevel) log.Debug().Msgf("Spinning up %d processes. Order: %q", len(runOrder), nameOrder) for _, proc := range runOrder { p.runProcess(proc) @@ -47,7 +49,11 @@ func (p *Project) runProcess(proc ProcessConfig) { if isStringDefined(proc.LogLocation) { procLogger = pclog.NewLogger(proc.LogLocation) } - process := NewProcess(p.Environment, procLogger, proc, p.GetProcessState(proc.Name), 1) + procLog, err := p.getProcessLog(proc.Name) + if err != nil { + procLog = pclog.NewLogBuffer(1000) + } + process := NewProcess(p.Environment, procLogger, proc, p.GetProcessState(proc.Name), procLog, 1) p.addRunningProcess(process) p.wg.Add(1) go func() { @@ -87,10 +93,12 @@ func (p *Project) initProcessStates() { p.processStates = make(map[string]*ProcessState) for key, proc := range p.Processes { p.processStates[key] = &ProcessState{ - Name: key, - Status: ProcessStatePending, - Restarts: 0, - ExitCode: 0, + Name: key, + Status: ProcessStatePending, + SystemTime: "", + Restarts: 0, + ExitCode: 0, + Pid: 0, } if proc.Disabled { p.processStates[key].Status = ProcessStateDisabled @@ -98,10 +106,25 @@ func (p *Project) initProcessStates() { } } +func (p *Project) initProcessLogs() { + p.processLogs = make(map[string]*pclog.ProcessLogBuffer) + for key := range p.Processes { + p.processLogs[key] = pclog.NewLogBuffer(1000) + } +} + func (p *Project) GetProcessState(name string) *ProcessState { if procState, ok := p.processStates[name]; ok { + proc := p.getRunningProcess(name) + if proc != nil { + proc.updateProcState() + } else { + procState.Pid = 0 + procState.SystemTime = "" + } return procState } + log.Error().Msgf("Error: process %s doesn't exist", name) return nil } @@ -153,6 +176,22 @@ func (p *Project) StopProcess(name string) error { return nil } +func (p *Project) getProcessLog(name string) (*pclog.ProcessLogBuffer, error) { + if procLogs, ok := p.processLogs[name]; ok { + return procLogs, nil + } + log.Error().Msgf("Error: process %s doesn't exist", name) + return nil, fmt.Errorf("process %s doesn't exist", name) +} + +func (p *Project) GetProcessLog(name string, offsetFromEnd, limit int) ([]string, error) { + logs, err := p.getProcessLog(name) + if err != nil { + return nil, err + } + return logs.GetLog(offsetFromEnd, limit), nil +} + func (p *Project) getProcesses(names ...string) ([]ProcessConfig, error) { processes := []ProcessConfig{} if len(names) == 0 { @@ -222,14 +261,14 @@ func (p *Project) GetDependenciesOrderNames() ([]string, error) { return order, err } -func (p *Project) GetLexicographicProcessNames() ([]string, error) { +func (p *Project) GetLexicographicProcessNames() []string { names := []string{} for name := range p.Processes { names = append(names, name) } sort.Strings(names) - return names, nil + return names } func CreateProject(inputFile string) *Project { diff --git a/src/main.go b/src/main.go index 217518a..f9e36a3 100644 --- a/src/main.go +++ b/src/main.go @@ -10,6 +10,7 @@ import ( "github.com/f1bonacc1/process-compose/src/api" "github.com/f1bonacc1/process-compose/src/app" + "github.com/f1bonacc1/process-compose/src/tui" "github.com/gin-gonic/gin" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -23,7 +24,7 @@ func setupLogger() { Out: os.Stdout, TimeFormat: "06-01-02 15:04:05", }) - zerolog.SetGlobalLevel(zerolog.InfoLevel) + zerolog.SetGlobalLevel(zerolog.DebugLevel) } func isFlagPassed(name string) bool { @@ -40,11 +41,28 @@ func init() { setupLogger() } +func quiet() func() { + null, _ := os.Open(os.DevNull) + sout := os.Stdout + serr := os.Stderr + os.Stdout = null + os.Stderr = null + zerolog.SetGlobalLevel(zerolog.Disabled) + return func() { + defer null.Close() + os.Stdout = sout + os.Stderr = serr + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } +} + func main() { fileName := "" port := 8080 + isTui := true flag.StringVar(&fileName, "f", app.DefaultFileNames[0], "path to file to load") flag.IntVar(&port, "p", port, "port number") + flag.BoolVar(&isTui, "t", isTui, "disable tui (-t=false)") flag.Parse() if !isFlagPassed("f") { pwd, err := os.Getwd() @@ -80,5 +98,13 @@ func main() { go server.ListenAndServe() project := app.CreateProject(fileName) - project.Run() + + if isTui { + defer quiet()() + go project.Run() + tui.SetupTui() + } else { + project.Run() + } + } diff --git a/src/pclog/process_log_buffer.go b/src/pclog/process_log_buffer.go new file mode 100644 index 0000000..fd95a24 --- /dev/null +++ b/src/pclog/process_log_buffer.go @@ -0,0 +1,50 @@ +package pclog + +const ( + slack = 100 +) + +type ProcessLogBuffer struct { + buffer []string + size int +} + +func NewLogBuffer(size int) *ProcessLogBuffer { + return &ProcessLogBuffer{ + size: size, + buffer: make([]string, 0, size), + } +} + +func (b *ProcessLogBuffer) Write(message string) { + b.buffer = append(b.buffer, message) + if len(b.buffer) > b.size+slack { + b.buffer = b.buffer[slack:] + } +} + +func (b ProcessLogBuffer) GetLog(offsetFromEnd, limit int) []string { + if len(b.buffer) == 0 { + return []string{} + } + if offsetFromEnd < 0 { + offsetFromEnd = 0 + } + if offsetFromEnd > len(b.buffer) { + offsetFromEnd = len(b.buffer) + } + + if limit < 1 { + limit = 0 + } + if limit > len(b.buffer) { + limit = len(b.buffer) + } + if offsetFromEnd+limit > len(b.buffer) { + limit = len(b.buffer) - offsetFromEnd + } + if limit == 0 { + return b.buffer[len(b.buffer)-offsetFromEnd:] + } + return b.buffer[len(b.buffer)-offsetFromEnd : offsetFromEnd+limit] +} diff --git a/src/tui/view.go b/src/tui/view.go new file mode 100644 index 0000000..cfde6c1 --- /dev/null +++ b/src/tui/view.go @@ -0,0 +1,177 @@ +package tui + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/f1bonacc1/process-compose/src/app" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type pcView struct { + procTable *tview.Table + statTable *tview.Table + appView *tview.Application + logsText *tview.TextView + statusText *tview.TextView + procNames []string +} + +func newPcView() *pcView { + pv := &pcView{ + appView: tview.NewApplication(), + logsText: tview.NewTextView().SetDynamicColors(true).SetScrollable(true), + statusText: tview.NewTextView().SetDynamicColors(true), + procNames: app.PROJ.GetLexicographicProcessNames(), + } + pv.procTable = pv.createProcTable() + pv.statTable = pv.createStatTable() + pv.appView.SetRoot(pv.createGrid(), true).EnableMouse(true). + SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyF10: + pv.appView.Stop() + } + return event + }) + if len(pv.procNames) > 0 { + pv.logsText.SetTitle(pv.procNames[0]) + } + return pv +} + +func (pv *pcView) fillTableData() { + if app.PROJ == nil { + return + } + + for r, name := range pv.procNames { + state := app.PROJ.GetProcessState(name) + if state == nil { + return + } + pv.procTable.SetCell(r+1, 0, tview.NewTableCell(strconv.Itoa(state.Pid)).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue)) + pv.procTable.SetCell(r+1, 1, tview.NewTableCell(state.Name).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue)) + pv.procTable.SetCell(r+1, 2, tview.NewTableCell(state.Status).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue)) + pv.procTable.SetCell(r+1, 3, tview.NewTableCell(state.SystemTime).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue)) + pv.procTable.SetCell(r+1, 4, tview.NewTableCell(strconv.Itoa(state.Restarts)).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue)) + pv.procTable.SetCell(r+1, 5, tview.NewTableCell(strconv.Itoa(state.ExitCode)).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue)) + } +} + +func (pv pcView) getSelectedProcName() string { + if pv.procTable == nil { + return "" + } + row, _ := pv.procTable.GetSelection() + if row > 0 && row <= len(pv.procNames) { + return pv.procNames[row-1] + } + return "" +} + +func (pv *pcView) fillLogs() { + name := pv.getSelectedProcName() + logs, err := app.PROJ.GetProcessLog(name, 1000, 0) + if err != nil { + pv.logsText.SetBorder(true).SetTitle(err.Error()) + pv.logsText.Clear() + } else { + pv.logsText.SetText(strings.Join(logs, "\n")) + } +} + +func (pv *pcView) onTableSelectionChange(row, column int) { + name := pv.getSelectedProcName() + pv.logsText.SetBorder(true).SetTitle(name) +} + +func (pv *pcView) createProcTable() *tview.Table { + table := tview.NewTable().SetBorders(false).SetSelectable(true, false).SetSelectionChangedFunc(pv.onTableSelectionChange) + //pv.fillTableData() + table.Select(1, 1).SetFixed(1, 0).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyF9: + name := pv.getSelectedProcName() + app.PROJ.StopProcess(name) + case tcell.KeyF7: + name := pv.getSelectedProcName() + app.PROJ.StartProcess(name) + } + return event + }) + columns := []string{ + "PID", "NAME", "STATUS", "TIME", "RESTARTS", "EXIT CODE", + } + for i := 0; i < len(columns); i++ { + table.SetCell(0, i, tview.NewTableCell(columns[i]). + SetSelectable(false).SetExpansion(1)) + } + return table +} + +func (pv *pcView) createStatTable() *tview.Table { + table := tview.NewTable().SetBorders(false).SetSelectable(false, false) + + table.SetCell(0, 0, tview.NewTableCell("Version:").SetSelectable(false).SetTextColor(tcell.ColorYellow)) + table.SetCell(0, 1, tview.NewTableCell("v0.7.2").SetSelectable(false)) + + table.SetCell(1, 0, tview.NewTableCell("Hostname:").SetSelectable(false).SetTextColor(tcell.ColorYellow)) + table.SetCell(1, 1, tview.NewTableCell("hhhppp").SetSelectable(false)) + + table.SetCell(2, 0, tview.NewTableCell("Processes:").SetSelectable(false).SetTextColor(tcell.ColorYellow)) + table.SetCell(2, 1, tview.NewTableCell(strconv.Itoa(len(pv.procNames))).SetSelectable(false)) + + table.SetCell(0, 3, tview.NewTableCell("🔥 Process Compose"). + SetSelectable(false). + SetAlign(tview.AlignRight). + SetExpansion(1). + SetTextColor(tcell.ColorYellow)) + return table +} + +func getHelpTextView() *tview.TextView { + textView := tview.NewTextView(). + SetDynamicColors(true) + fmt.Fprintf(textView, "%s ", "F7[black:green]Start[-:-:-]") + fmt.Fprintf(textView, "%s ", "F9[black:green]Kill[-:-:-]") + fmt.Fprintf(textView, "%s ", "F10[black:green]Quit[-:-:-]") + return textView +} + +func (pv pcView) createGrid() *tview.Grid { + grid := tview.NewGrid(). + SetRows(3, 0, 0, 1). + //SetColumns(30, 0, 30). + SetBorders(true). + AddItem(pv.statTable, 0, 0, 1, 1, 0, 0, false). + AddItem(pv.procTable, 1, 0, 1, 1, 0, 0, true). + AddItem(pv.logsText, 2, 0, 1, 1, 0, 0, false). + AddItem(getHelpTextView(), 3, 0, 1, 1, 0, 0, false) + + grid.SetTitle("Process Compose") + return grid +} + +func (pv *pcView) updateTable() { + for { + time.Sleep(1000 * time.Millisecond) + pv.appView.QueueUpdateDraw(func() { + pv.fillTableData() + pv.fillLogs() + }) + } +} + +func SetupTui() { + pv := newPcView() + + go pv.updateTable() + + if err := pv.appView.Run(); err != nil { + panic(err) + } +}