mirror of
https://github.com/F1bonacc1/process-compose.git
synced 2024-08-16 15:00:32 +03:00
TUI Support
This commit is contained in:
parent
296cfd0c71
commit
63c0e61eb4
8
Makefile
8
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}*
|
||||
|
8
go.mod
8
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
|
||||
)
|
||||
|
17
go.sum
17
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=
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
67
src/app/process_test.go
Normal file
67
src/app/process_test.go
Normal file
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
30
src/main.go
30
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
50
src/pclog/process_log_buffer.go
Normal file
50
src/pclog/process_log_buffer.go
Normal file
@ -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]
|
||||
}
|
177
src/tui/view.go
Normal file
177
src/tui/view.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user