switch interactive shell to PTY to support cases like python or neo4j

This commit is contained in:
sqshq 2019-05-13 23:24:29 -04:00
parent 809d361ca7
commit 4d5fa35642
9 changed files with 104 additions and 50 deletions

View File

@ -34,7 +34,12 @@ func NewStatusLine(configFileName string, palette console.Palette) *StatusBar {
func (s *StatusBar) Draw(buffer *ui.Buffer) { func (s *StatusBar) Draw(buffer *ui.Buffer) {
buffer.Fill(ui.NewCell(' ', ui.NewStyle(console.ColorClear, console.MenuColorBackground)), s.GetRect()) buffer.Fill(ui.NewCell(' ', ui.NewStyle(console.ColorClear, console.MenuColorBackground)), s.GetRect())
buffer.SetString(fmt.Sprintf(" %s %s @ %s", console.AppTitle, console.AppVersion, s.configFileName), ui.NewStyle(console.MenuColorText, console.MenuColorBackground), s.Min)
if false { // TODO check license
buffer.SetString(fmt.Sprintf(" %s", console.AppLicenseWarning), ui.NewStyle(console.MenuColorText, console.MenuColorBackground), s.Min)
} else {
buffer.SetString(fmt.Sprintf(" %s %s @ %s", console.AppTitle, console.AppVersion, s.configFileName), ui.NewStyle(console.MenuColorText, console.MenuColorBackground), s.Min)
}
indent := bindingsIndent indent := bindingsIndent
for _, binding := range s.keyBindings { for _, binding := range s.keyBindings {

View File

@ -0,0 +1,29 @@
variables:
PGPASSWORD: fred
mongoconnection: mongo --quiet --host=localhost blog
mysqlconnection: mysql -u root -s --database mysql --skip-column-names
neo4jconnection: cypher-shell -u neo4j -p 121314 --format plain
postgresconnection: psql -h localhost -U postgres --no-align --tuples-only
sshconnection: ssh -i ~/sqshq.pem ec2-user@3.215.108.82
textboxes:
- title: Neo4j
position: [[0, 0], [13, 40]]
init: $neo4jconnection
sample: match (n) return count(n);
transform: echo "$sample" | tail -n 1
- title: Postgres
position: [[13, 0], [14, 40]]
init: $postgresconnection
sample: select random();
- title: MySQL
position: [[27, 0], [14, 40]]
init: $mysqlconnection
sample: select rand();
- title: MongoDB
position: [[41, 0], [13, 40]]
init: $mongoconnection
sample: db.getCollection('posts').find({status:'ACTIVE'}).itcount()
- title: SSH
position: [[54, 0], [13, 40]]
init: $sshconnection
sample: ps -A -o %cpu | awk '{s+=$1} END {print s}'

View File

@ -14,6 +14,7 @@ const (
RowsCount = 40 RowsCount = 40
AppTitle = "sampler" AppTitle = "sampler"
AppVersion = "0.9.0" AppVersion = "0.9.0"
AppLicenseWarning = "UNLICENSED. FOR PERSONAL USE ONLY. VISIT SAMPLER.DEV"
) )
const ( const (

View File

@ -3,7 +3,9 @@ package data
import ( import (
"bufio" "bufio"
"errors" "errors"
"fmt"
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"github.com/kr/pty"
"github.com/sqshq/sampler/config" "github.com/sqshq/sampler/config"
"io" "io"
"os" "os"
@ -12,6 +14,8 @@ import (
"time" "time"
) )
const interactiveShellStartupTimeout = time.Second
type Item struct { type Item struct {
Label string Label string
SampleScript string SampleScript string
@ -23,10 +27,9 @@ type Item struct {
} }
type InteractiveShell struct { type InteractiveShell struct {
StdoutCh chan string Channel chan string
StderrCh chan string File io.WriteCloser
Stdin io.WriteCloser Cmd *exec.Cmd
Cmd *exec.Cmd
} }
func NewItems(cfgs []config.Item, rateMs int) []*Item { func NewItems(cfgs []config.Item, rateMs int) []*Item {
@ -53,7 +56,7 @@ func (i *Item) nextValue(variables []string) (string, error) {
if i.InitScript != nil && i.InteractiveShell == nil { if i.InitScript != nil && i.InteractiveShell == nil {
err := i.initInteractiveShell(variables) err := i.initInteractiveShell(variables)
if err != nil { if err != nil {
return "", err return "", errors.New(fmt.Sprintf("Failed to init interactive shell: %s", err))
} }
} }
@ -83,84 +86,62 @@ func (i *Item) initInteractiveShell(variables []string) error {
cmd := exec.Command("sh", "-c", *i.InitScript) cmd := exec.Command("sh", "-c", *i.InitScript)
enrichEnvVariables(cmd, variables) enrichEnvVariables(cmd, variables)
stdout, err := cmd.StdoutPipe() file, err := pty.Start(cmd)
if err != nil { if err != nil {
return err return err
} }
stderr, err := cmd.StderrPipe() scanner := bufio.NewScanner(file)
if err != nil { channel := make(chan string)
return err
}
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
stdoutCh := make(chan string)
stderrCh := make(chan string)
go func() { go func() {
for stdoutScanner.Scan() { for scanner.Scan() {
stdoutCh <- stdoutScanner.Text() channel <- scanner.Text()
stderrCh <- stderrScanner.Text()
} }
}() }()
i.InteractiveShell = &InteractiveShell{ i.InteractiveShell = &InteractiveShell{
StdoutCh: stdoutCh, Channel: channel,
StderrCh: stderrCh, File: file,
Stdin: stdin, Cmd: cmd,
Cmd: cmd,
} }
err = cmd.Start() _, err = file.Read(make([]byte, 4096))
if err != nil { if err != nil {
return err return err
} }
time.Sleep(interactiveShellStartupTimeout)
return nil return nil
} }
func (i *Item) executeInteractiveShellCmd(variables []string) (string, error) { func (i *Item) executeInteractiveShellCmd(variables []string) (string, error) {
_, err := io.WriteString(i.InteractiveShell.Stdin, i.SampleScript+"\n") _, err := io.WriteString(i.InteractiveShell.File, fmt.Sprintf(" %s\n", i.SampleScript))
if err != nil { if err != nil {
return "", err return "", errors.New(fmt.Sprintf("Failed to execute interactive shell cmd: %s", err))
} }
timeout := make(chan bool, 1) timeout := make(chan bool, 1)
go func() { go func() {
time.Sleep(time.Duration(i.RateMs / 2)) time.Sleep(time.Duration(i.RateMs))
timeout <- true timeout <- true
}() }()
var resultText strings.Builder var outputText strings.Builder
var errorText strings.Builder
for { for {
select { select {
case stdout := <-i.InteractiveShell.StdoutCh: case output := <-i.InteractiveShell.Channel:
if len(stdout) > 0 { if !strings.Contains(output, i.SampleScript) && len(output) > 0 {
resultText.WriteString(stdout) outputText.WriteString(output)
resultText.WriteString("\n") outputText.WriteString("\n")
}
case stderr := <-i.InteractiveShell.StderrCh:
if len(stderr) > 0 {
errorText.WriteString(stderr)
errorText.WriteString("\n")
} }
case <-timeout: case <-timeout:
if errorText.Len() > 0 { sample := cleanupOutput(outputText.String())
return "", errors.New(errorText.String()) return i.transformInteractiveShellCmd(sample)
} else {
return i.transformInteractiveShellCmd(resultText.String())
}
} }
} }
} }
@ -180,3 +161,11 @@ func enrichEnvVariables(cmd *exec.Cmd, variables []string) {
cmd.Env = append(cmd.Env, variable) cmd.Env = append(cmd.Env, variable)
} }
} }
func cleanupOutput(output string) string {
s := strings.TrimSpace(output)
if idx := strings.Index(s, "\r"); idx != -1 {
return s[idx+1:]
}
return s
}

25
data/item_test.go Normal file
View File

@ -0,0 +1,25 @@
package data
import "testing"
func Test_cleanupOutput(t *testing.T) {
type args struct {
output string
}
tests := []struct {
name string
args args
want string
}{
{"should trim everything before carriage return", args{">>\rtext"}, "text"},
{"should trim carriage return at the end", args{"text\r"}, "text"},
{"should remove tabs and spaces", args{"\t\t\ntext "}, "text"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cleanupOutput(tt.args.output); got != tt.want {
t.Errorf("cleanupOutput() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -39,7 +39,7 @@ func NewSampler(consumer *Consumer, items []*Item, triggers []*Trigger, options
select { select {
case sample := <-sampler.triggersChannel: case sample := <-sampler.triggersChannel:
for _, t := range sampler.triggers { for _, t := range sampler.triggers {
t.Execute(sample) go t.Execute(sample)
} }
} }
} }

1
go.mod
View File

@ -5,6 +5,7 @@ require (
github.com/hajimehoshi/go-mp3 v0.1.1 github.com/hajimehoshi/go-mp3 v0.1.1
github.com/hajimehoshi/oto v0.1.1 github.com/hajimehoshi/oto v0.1.1
github.com/jessevdk/go-flags v1.4.0 github.com/jessevdk/go-flags v1.4.0
github.com/kr/pty v1.1.4
github.com/mattn/go-runewidth v0.0.4 github.com/mattn/go-runewidth v0.0.4
github.com/mbndr/figlet4go v0.0.0-20190224160619-d6cef5b186ea github.com/mbndr/figlet4go v0.0.0-20190224160619-d6cef5b186ea
github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect

2
go.sum
View File

@ -12,6 +12,8 @@ github.com/hajimehoshi/oto v0.1.1 h1:EG+WxxeAfde1mI0adhLYvGbKgDCxm7bCTd6g+JIA6vI
github.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQdfpwz04= github.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQdfpwz04=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=

View File

@ -15,6 +15,7 @@ import (
"github.com/sqshq/sampler/console" "github.com/sqshq/sampler/console"
"github.com/sqshq/sampler/data" "github.com/sqshq/sampler/data"
"github.com/sqshq/sampler/event" "github.com/sqshq/sampler/event"
"time"
) )
type Starter struct { type Starter struct {
@ -30,6 +31,7 @@ func (s *Starter) start(drawable ui.Drawable, consumer *data.Consumer, component
items := data.NewItems(itemsConfig, *componentConfig.RateMs) items := data.NewItems(itemsConfig, *componentConfig.RateMs)
data.NewSampler(consumer, items, triggers, s.opt, s.cfg.Variables, *componentConfig.RateMs) data.NewSampler(consumer, items, triggers, s.opt, s.cfg.Variables, *componentConfig.RateMs)
s.lout.AddComponent(cpt) s.lout.AddComponent(cpt)
time.Sleep(100 * time.Millisecond) // desync coroutines
} }
func main() { func main() {