From 7db33312b6bb47f64defb6cac8bf51ebb19aaaf5 Mon Sep 17 00:00:00 2001 From: sqshq Date: Sat, 9 Mar 2019 23:41:23 -0500 Subject: [PATCH] refactoring, added sequential triggers execution per component --- asset/player.go | 45 ++++++++----- component/runchart/runchart.go | 2 - config.yml | 20 ++++-- config/default.go | 16 ++--- console/console.go | 4 ++ data/consumer.go | 3 +- data/item.go | 15 ++++- data/sampler.go | 58 ++++++++++------ data/trigger.go | 120 +++++++++++++++++++++++++++++++++ main.go | 35 +++++----- trigger/trigger.go | 111 ------------------------------ 11 files changed, 246 insertions(+), 183 deletions(-) create mode 100644 data/trigger.go delete mode 100644 trigger/trigger.go diff --git a/asset/player.go b/asset/player.go index 42b86da..df18a0c 100644 --- a/asset/player.go +++ b/asset/player.go @@ -7,28 +7,41 @@ import ( "log" ) -func Beep() error { +type AudioPlayer struct { + player *oto.Player + beep []byte +} + +func NewAudioPlayer() *AudioPlayer { bytes, err := Asset("quindar-tone.mp3") if err != nil { - log.Fatal("Can't find asset file") + log.Fatal("Can't find audio file") } - d, err := mp3.NewDecoder(NewAssetFile(bytes)) + player, err := oto.NewPlayer(44100, 2, 2, 8192) if err != nil { - return err - } - defer d.Close() - - p, err := oto.NewPlayer(d.SampleRate(), 2, 2, 8192) - if err != nil { - return err - } - defer p.Close() - - if _, err := io.Copy(p, d); err != nil { - return err + panic(err) } - return nil + return &AudioPlayer{ + player: player, + beep: bytes, + } +} + +func (a *AudioPlayer) Beep() { + + decoder, err := mp3.NewDecoder(NewAssetFile(a.beep)) + if err != nil { + panic(err) + } + + if _, err := io.Copy(a.player, decoder); err != nil { + panic(err) + } +} + +func (a *AudioPlayer) Close() { + _ = a.player.Close() } diff --git a/component/runchart/runchart.go b/component/runchart/runchart.go index b6fc0c8..ba99632 100644 --- a/component/runchart/runchart.go +++ b/component/runchart/runchart.go @@ -5,7 +5,6 @@ import ( "github.com/sqshq/sampler/config" "github.com/sqshq/sampler/console" "github.com/sqshq/sampler/data" - "github.com/sqshq/sampler/trigger" "image" "math" "strconv" @@ -39,7 +38,6 @@ const ( type RunChart struct { ui.Block data.Consumer - triggers []trigger.Trigger lines []TimeLine grid ChartGrid timescale time.Duration diff --git a/config.yml b/config.yml index 1d54c76..9743d1f 100644 --- a/config.yml +++ b/config.yml @@ -7,13 +7,13 @@ runcharts: w: 53 h: 16 triggers: - - title: PROCESSING STARTED # ${prev} ${cur} ${lavel} echo $(( 3 < 4 && 1 > 2 )) - condition: ((${prev} == 0 && ${cur} > 0)) - actions: - terminal-bell: true - sound: false - visual: true - script: say ${lavel} initiated with value ${cur} + - title: LATENCY + condition: echo "$prev < 0.4 && $cur > 0.4" |bc -l + actions: + terminal-bell: true + sound: true + visual: true + script: 'say alert: ${label} latency exceeded ${cur} second' scale: 3 items: - label: GOOGLE @@ -100,6 +100,12 @@ gauges: size: w: 27 h: 2 + triggers: + - title: CLOCK BELL EVERY MINUTE + condition: '[ $label == "cur" ] && [ $cur -eq 0 ] && echo 1' + actions: + sound: true + script: say -v samantha `date +%I:%M%p` values: cur: date +%S max: echo 60 diff --git a/config/default.go b/config/default.go index fb8cbe5..5057f55 100644 --- a/config/default.go +++ b/config/default.go @@ -104,9 +104,9 @@ func (c *Config) setDefaultValues() { func setDefaultTriggersValues(triggers []TriggerConfig) { - defaultTerminalBell := true + defaultTerminalBell := false defaultSound := false - defaultVisual := true + defaultVisual := false for i, trigger := range triggers { @@ -137,20 +137,20 @@ func (c *Config) setDefaultColors() { palette := console.GetPalette(*c.Theme) colorsCount := len(palette.Colors) - for _, chart := range c.RunCharts { - for j, item := range chart.Items { + for _, ch := range c.RunCharts { + for j, item := range ch.Items { if item.Color == nil { item.Color = &palette.Colors[j%colorsCount] - chart.Items[j] = item + ch.Items[j] = item } } } - for _, chart := range c.BarCharts { - for j, item := range chart.Items { + for _, b := range c.BarCharts { + for j, item := range b.Items { if item.Color == nil { item.Color = &palette.Colors[j%colorsCount] - chart.Items[j] = item + b.Items[j] = item } } } diff --git a/console/console.go b/console/console.go index a219179..be1134b 100644 --- a/console/console.go +++ b/console/console.go @@ -23,6 +23,10 @@ const ( type Console struct{} +const ( + BellCharacter = "\a" +) + func (self *Console) Init() { fmt.Printf("\033]0;%s\007", AppTitle) diff --git a/data/consumer.go b/data/consumer.go index f25dcef..5f13946 100644 --- a/data/consumer.go +++ b/data/consumer.go @@ -11,7 +11,8 @@ type Sample struct { } type Alert struct { - Text string + Title string + Text string } func NewConsumer() Consumer { diff --git a/data/item.go b/data/item.go index 86aaa8b..c870fe4 100644 --- a/data/item.go +++ b/data/item.go @@ -1,6 +1,7 @@ package data import ( + "github.com/sqshq/sampler/config" ui "github.com/sqshq/termui" "os/exec" "strings" @@ -9,7 +10,19 @@ import ( type Item struct { Label string Script string - Color ui.Color + Color *ui.Color +} + +func NewItems(cfgs []config.Item) []Item { + + items := make([]Item, 0) + + for _, i := range cfgs { + item := Item{Label: *i.Label, Script: i.Script, Color: i.Color} + items = append(items, item) + } + + return items } func (i *Item) nextValue() (value string, err error) { diff --git a/data/sampler.go b/data/sampler.go index 81356f6..a0168ab 100644 --- a/data/sampler.go +++ b/data/sampler.go @@ -1,43 +1,63 @@ package data import ( - "github.com/sqshq/sampler/trigger" "time" ) type Sampler struct { - consumer Consumer - item Item - triggers []trigger.Trigger + consumer Consumer + items []Item + triggers []Trigger + triggersChannel chan Sample } -func NewSampler(consumer Consumer, item Item, triggers []trigger.Trigger, rateMs int) Sampler { +func NewSampler(consumer Consumer, items []Item, triggers []Trigger, rateMs int) Sampler { - ticker := time.NewTicker(time.Duration(rateMs * int(time.Millisecond))) - sampler := Sampler{consumer, item, triggers} + ticker := time.NewTicker( + time.Duration(rateMs * int(time.Millisecond)), + ) + + sampler := Sampler{ + consumer, + items, + triggers, + make(chan Sample), + } go func() { - sampler.sample() for ; true; <-ticker.C { - sampler.sample() + for _, item := range sampler.items { + go sampler.sample(item) + } + } + }() + + go func() { + for { + select { + case sample := <-sampler.triggersChannel: + for _, t := range sampler.triggers { + t.Execute(sample) + } + } } }() return sampler } -func (s *Sampler) sample() { - value, err := s.item.nextValue() +func (s *Sampler) sample(item Item) { + + val, err := item.nextValue() + if err == nil { - - sample := Sample{Value: value, Label: s.item.Label} + sample := Sample{Label: item.Label, Value: val} s.consumer.SampleChannel <- sample - - for _, t := range s.triggers { - t.Execute(s.item.Label, value) - } - + s.triggersChannel <- sample } else { - s.consumer.AlertChannel <- Alert{Text: err.Error()} + s.consumer.AlertChannel <- Alert{ + Title: "SAMPLING FAILURE", + Text: err.Error(), + } } } diff --git a/data/trigger.go b/data/trigger.go new file mode 100644 index 0000000..c7c5fc7 --- /dev/null +++ b/data/trigger.go @@ -0,0 +1,120 @@ +package data + +import ( + "fmt" + "github.com/sqshq/sampler/asset" + "github.com/sqshq/sampler/config" + "github.com/sqshq/sampler/console" + "os" + "os/exec" + "regexp" +) + +const ( + TrueIndicator = "1" + InitialValue = "0" +) + +type Trigger struct { + title string + condition string + actions Actions + consumer Consumer + valuesByLabel map[string]Values + player *asset.AudioPlayer + digitsRegexp *regexp.Regexp +} + +type Actions struct { + terminalBell bool + sound bool + visual bool + script *string +} + +type Values struct { + current string + previous string +} + +func NewTriggers(cfgs []config.TriggerConfig, consumer Consumer, player *asset.AudioPlayer) []Trigger { + + triggers := make([]Trigger, 0) + + for _, cfg := range cfgs { + triggers = append(triggers, NewTrigger(cfg, consumer, player)) + } + + return triggers +} + +func NewTrigger(config config.TriggerConfig, consumer Consumer, player *asset.AudioPlayer) Trigger { + return Trigger{ + title: config.Title, + condition: config.Condition, + consumer: consumer, + valuesByLabel: make(map[string]Values), + player: player, + digitsRegexp: regexp.MustCompile("[^0-9]+"), + actions: Actions{ + terminalBell: *config.Actions.TerminalBell, + sound: *config.Actions.Sound, + visual: *config.Actions.Visual, + script: config.Actions.Script, + }, + } +} + +func (t *Trigger) Execute(sample Sample) { + if t.evaluate(sample) { + + if t.actions.terminalBell { + fmt.Print(console.BellCharacter) + } + + if t.actions.sound { + t.player.Beep() + } + + if t.actions.visual { + //t.consumer.AlertChannel <- Alert{ + // Title: "TRIGGER ALERT", Text: sample.Label, + //} + } + + if t.actions.script != nil { + _, _ = runScript(*t.actions.script, sample.Label, t.valuesByLabel[sample.Label]) + } + } +} + +func (t *Trigger) evaluate(sample Sample) bool { + + if values, ok := t.valuesByLabel[sample.Label]; ok { + values.previous = values.current + values.current = sample.Value + t.valuesByLabel[sample.Label] = values + } else { + t.valuesByLabel[sample.Label] = Values{previous: InitialValue, current: sample.Value} + } + + output, err := runScript(t.condition, sample.Label, t.valuesByLabel[sample.Label]) + + if err != nil { + //t.consumer.AlertChannel <- Alert{Title: "TRIGGER CONDITION FAILURE", Text: err.Error()} + } + + return t.digitsRegexp.ReplaceAllString(string(output), "") == TrueIndicator +} + +func runScript(script, label string, data Values) ([]byte, error) { + + cmd := exec.Command("sh", "-c", script) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, + fmt.Sprintf("prev=%v", data.previous), + fmt.Sprintf("cur=%v", data.current), + fmt.Sprintf("label=%v", label)) + + return cmd.Output() +} diff --git a/main.go b/main.go index be804b4..49ef9b2 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/sqshq/sampler/asset" "github.com/sqshq/sampler/component" "github.com/sqshq/sampler/component/asciibox" "github.com/sqshq/sampler/component/barchart" @@ -11,7 +12,6 @@ import ( "github.com/sqshq/sampler/console" "github.com/sqshq/sampler/data" "github.com/sqshq/sampler/event" - "github.com/sqshq/sampler/trigger" ui "github.com/sqshq/termui" ) @@ -25,51 +25,50 @@ func main() { width, height := ui.TerminalDimensions() lout := layout.NewLayout(width, height, component.NewStatusLine(flg.ConfigFileName), component.NewMenu()) + player := asset.NewAudioPlayer() + defer player.Close() + for _, c := range cfg.RunCharts { legend := runchart.Legend{Enabled: c.Legend.Enabled, Details: c.Legend.Details} chart := runchart.NewRunChart(c, legend) lout.AddComponent(config.TypeRunChart, chart, c.Title, c.Position, c.Size, *c.RefreshRateMs) - triggers := trigger.NewTriggers(c.Triggers) + triggers := data.NewTriggers(c.Triggers, chart.Consumer, player) + items := data.NewItems(c.Items) + data.NewSampler(chart.Consumer, items, triggers, *c.RefreshRateMs) for _, i := range c.Items { - item := data.Item{Label: *i.Label, Script: i.Script, Color: *i.Color} - chart.AddLine(item.Label, item.Color) - data.NewSampler(chart.Consumer, item, triggers, *c.RefreshRateMs) + chart.AddLine(*i.Label, *i.Color) } } for _, a := range cfg.AsciiBoxes { box := asciibox.NewAsciiBox(a) - item := data.Item{Label: *a.Label, Script: a.Script, Color: *a.Color} - triggers := trigger.NewTriggers(a.Triggers) + item := data.Item{Label: *a.Label, Script: a.Script, Color: a.Color} lout.AddComponent(config.TypeAsciiBox, box, a.Title, a.Position, a.Size, *a.RefreshRateMs) - data.NewSampler(box.Consumer, item, triggers, *a.RefreshRateMs) + triggers := data.NewTriggers(a.Triggers, box.Consumer, player) + data.NewSampler(box.Consumer, []data.Item{item}, triggers, *a.RefreshRateMs) } for _, b := range cfg.BarCharts { chart := barchart.NewBarChart(b.Title, *b.Scale) - triggers := trigger.NewTriggers(b.Triggers) + triggers := data.NewTriggers(b.Triggers, chart.Consumer, player) lout.AddComponent(config.TypeBarChart, chart, b.Title, b.Position, b.Size, *b.RefreshRateMs) + items := data.NewItems(b.Items) + data.NewSampler(chart.Consumer, items, triggers, *b.RefreshRateMs) for _, i := range b.Items { - item := data.Item{Label: *i.Label, Script: i.Script, Color: *i.Color} chart.AddBar(*i.Label, *i.Color) - data.NewSampler(chart.Consumer, item, triggers, *b.RefreshRateMs) } } for _, gc := range cfg.Gauges { - g := gauge.NewGauge(gc.Title, *gc.Scale, *gc.Color) - triggers := trigger.NewTriggers(gc.Triggers) + triggers := data.NewTriggers(gc.Triggers, g.Consumer, player) lout.AddComponent(config.TypeGauge, g, gc.Title, gc.Position, gc.Size, *gc.RefreshRateMs) - - for _, i := range gc.Items { - item := data.Item{Label: *i.Label, Script: i.Script} - data.NewSampler(g.Consumer, item, triggers, *gc.RefreshRateMs) - } + items := data.NewItems(gc.Items) + data.NewSampler(g.Consumer, items, triggers, *gc.RefreshRateMs) } handler := event.NewHandler(lout) diff --git a/trigger/trigger.go b/trigger/trigger.go deleted file mode 100644 index 47330a8..0000000 --- a/trigger/trigger.go +++ /dev/null @@ -1,111 +0,0 @@ -package trigger - -import ( - "fmt" - "github.com/sqshq/sampler/asset" - "github.com/sqshq/sampler/config" - "os" - "os/exec" -) - -const ( - TrueIndicator = "1" - BellCharacter = "\a" -) - -type Trigger struct { - title string - condition string - actions Actions - data map[string]Data -} - -type Actions struct { - terminalBell bool - sound bool - visual bool - script *string -} - -type Data struct { - previousValue interface{} - currentValue interface{} -} - -func NewTriggers(configs []config.TriggerConfig) []Trigger { - - triggers := make([]Trigger, len(configs)) - - for _, c := range configs { - triggers = append(triggers, NewTrigger(c)) - } - - return triggers -} - -func NewTrigger(config config.TriggerConfig) Trigger { - return Trigger{ - title: config.Title, - condition: config.Condition, - data: make(map[string]Data), - actions: Actions{ - terminalBell: *config.Actions.TerminalBell, - sound: *config.Actions.Sound, - visual: *config.Actions.Visual, - script: config.Actions.Script, - }, - } -} - -func (t Trigger) Execute(label string, value interface{}) { - - if data, ok := t.data[label]; ok { - data.previousValue = data.currentValue - data.currentValue = value - } else { - t.data[label] = Data{previousValue: nil, currentValue: value} - } - - t.evaluate(label, t.data[label]) -} - -func (t Trigger) evaluate(label string, data Data) { - - output, err := runScript(t.condition, label, data) - - if err != nil { - //println(err) // TODO visual notification - } - - if string(output) != TrueIndicator { - return - } - - if t.actions.terminalBell { - fmt.Print(BellCharacter) - } - - if t.actions.sound { - _ = asset.Beep() - } - - if t.actions.visual { - // TODO visual notification - } - - if t.actions.script != nil { - _, _ = runScript(*t.actions.script, label, data) - } -} - -func runScript(script, label string, data Data) ([]byte, error) { - cmd := exec.Command("sh", "-c", script) - - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, - fmt.Sprintf("prev=%v", data.previousValue), - fmt.Sprintf("cur=%v", data.currentValue), - fmt.Sprintf("label=%v", label)) - - return cmd.Output() -}