Untested cli/daemon communication code
This commit is contained in:
parent
4c86f37b70
commit
f3080f5293
119
app/cli.go
Normal file
119
app/cli.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Query = 0
|
||||||
|
Flush = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Request int
|
||||||
|
Id int
|
||||||
|
Pattern string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Err error
|
||||||
|
Actions ReadableMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runtime files:
|
||||||
|
// /run/user/<uid>/reaction/reaction.pipe
|
||||||
|
// /run/user/<uid>/reaction/id.response
|
||||||
|
|
||||||
|
func RuntimeDirectory() string {
|
||||||
|
return fmt.Sprintf("/run/user/%v/reaction/", os.Getuid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func PipePath() string {
|
||||||
|
return path.Join(RuntimeDirectory(), "reaction.pipe")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Request) ResponsePath() string {
|
||||||
|
return path.Join(RuntimeDirectory(), string(r.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Send(data Request) {
|
||||||
|
pipePath := PipePath()
|
||||||
|
pipe, err := os.OpenFile(pipePath, os.O_APPEND, os.ModeNamedPipe)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to open", pipePath, ":", err)
|
||||||
|
log.Fatalln("Is the reaction daemon running? Does the CLI run as the same user?")
|
||||||
|
}
|
||||||
|
log.Println("DEBUG opening ok, encoding...")
|
||||||
|
enc := gob.NewEncoder(pipe)
|
||||||
|
err = enc.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to write to %s: %s", pipePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendAndRetrieve(data Request) Response {
|
||||||
|
if data.Id == 0 {
|
||||||
|
data.Id = rand.Int()
|
||||||
|
}
|
||||||
|
log.Println("DEBUG sending:", data)
|
||||||
|
Send(data)
|
||||||
|
responsePath := data.ResponsePath()
|
||||||
|
d, _ := time.ParseDuration("100ms")
|
||||||
|
for tries := 20; tries > 0; tries-- {
|
||||||
|
log.Println("DEBUG waiting for answer...")
|
||||||
|
file, err := os.Open(responsePath)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
time.Sleep(d)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer os.Remove(responsePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error opening daemon answer: %s", err)
|
||||||
|
}
|
||||||
|
var response Response
|
||||||
|
err = gob.NewDecoder(file).Decode(&response)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error parsing daemon answer: %s", err)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
log.Fatalln("Timeout while waiting answer from the daemon")
|
||||||
|
return Response{errors.New("unreachable code"), nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage(err string) {
|
||||||
|
fmt.Println("Usage: reactionc")
|
||||||
|
fmt.Println("Usage: reactionc flush <PATTERN>")
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CLI() {
|
||||||
|
if len(os.Args) <= 1 {
|
||||||
|
response := SendAndRetrieve(Request{Query, 0, ""})
|
||||||
|
if response.Err != nil {
|
||||||
|
log.Fatalln("Received error from daemon:", response.Err)
|
||||||
|
}
|
||||||
|
fmt.Println(response.Actions.ToString())
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "flush":
|
||||||
|
if len(os.Args) != 3 {
|
||||||
|
usage("flush takes one <PATTERN> argument")
|
||||||
|
}
|
||||||
|
response := SendAndRetrieve(Request{Flush, 0, os.Args[2]})
|
||||||
|
if response.Err != nil {
|
||||||
|
log.Fatalln("Received error from daemon:", response.Err)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
default:
|
||||||
|
usage("first argument must be `flush`")
|
||||||
|
}
|
||||||
|
}
|
174
app/pipe.go
Normal file
174
app/pipe.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionMap map[string]map[*Action]map[chan bool]bool
|
||||||
|
type ReadableMap map[string]map[string]map[string]int
|
||||||
|
|
||||||
|
type ActionStore struct {
|
||||||
|
store ActionMap
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by an Action before entering sleep
|
||||||
|
func (a *ActionStore) Register(action *Action, pattern string) chan bool {
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
if a.store[pattern] == nil {
|
||||||
|
a.store[pattern] = make(map[*Action]map[chan bool]bool)
|
||||||
|
}
|
||||||
|
if a.store[pattern][action] == nil {
|
||||||
|
a.store[pattern][action] = make(map[chan bool]bool)
|
||||||
|
}
|
||||||
|
sig := make(chan bool)
|
||||||
|
a.store[pattern][action][sig] = true
|
||||||
|
return sig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by an Action after sleep
|
||||||
|
func (a *ActionStore) Unregister(action *Action, pattern string, sig chan bool) {
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
if a.store[pattern] == nil || a.store[pattern][action] == nil || len(a.store[pattern][action]) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
close(sig)
|
||||||
|
delete(a.store[pattern][action], sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by Main
|
||||||
|
func (a *ActionStore) Quit() {
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
for _, actions := range a.store {
|
||||||
|
for _, sigs := range actions {
|
||||||
|
for sig := range sigs {
|
||||||
|
close(sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.store = make(ActionMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by a CLI
|
||||||
|
func (a *ActionStore) Flush(pattern string) {
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
if a.store[pattern] != nil {
|
||||||
|
for _, action := range a.store[pattern] {
|
||||||
|
for sig := range action {
|
||||||
|
close(sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(a.store, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by a CLI
|
||||||
|
func (a *ActionStore) pendingActions() ReadableMap {
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
return a.store.ToReadable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ActionMap) ToReadable() ReadableMap {
|
||||||
|
res := make(ReadableMap)
|
||||||
|
|
||||||
|
for pattern, actions := range a {
|
||||||
|
for action := range actions {
|
||||||
|
filter := action.filter.name
|
||||||
|
stream := action.filter.stream.name
|
||||||
|
if res[stream] == nil {
|
||||||
|
res[stream] = make(map[string]map[string]int)
|
||||||
|
}
|
||||||
|
if res[stream][filter] == nil {
|
||||||
|
res[stream][filter] = make(map[string]int)
|
||||||
|
}
|
||||||
|
res[stream][filter][pattern] = res[stream][filter][pattern] + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadableMap) ToString() string {
|
||||||
|
text, err := yaml.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
return string(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pipe-related, server-related functions
|
||||||
|
|
||||||
|
func createOpenPipe() fs.File {
|
||||||
|
err := os.Mkdir(RuntimeDirectory(), 0755)
|
||||||
|
if err != nil && !errors.Is(err, os.ErrExist) {
|
||||||
|
log.Fatalln("FATAL Failed to create runtime directory", err)
|
||||||
|
}
|
||||||
|
pipePath := PipePath()
|
||||||
|
_, err = os.Stat(pipePath)
|
||||||
|
if err == nil {
|
||||||
|
log.Println("WARN Runtime file", pipePath, "already exists: Is the daemon already running? Deleting.")
|
||||||
|
err = os.Remove(pipePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("FATAL Failed to remove runtime file:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = syscall.Mkfifo(pipePath, 0600)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("FATAL Failed to create runtime file:", err)
|
||||||
|
}
|
||||||
|
file, err := os.OpenFile(pipePath, os.O_RDONLY, os.ModeNamedPipe)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("FATAL Failed to open runtime file:", err)
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func Respond(request Request, response Response) {
|
||||||
|
file, err := os.Create(request.ResponsePath())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("WARN Can't respond to message:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = gob.NewEncoder(file).Encode(response)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("WARN Can't respond to message:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle connections
|
||||||
|
func Serve() {
|
||||||
|
pipe := createOpenPipe()
|
||||||
|
for {
|
||||||
|
var request Request
|
||||||
|
err := gob.NewDecoder(pipe).Decode(&request)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("WARN Invalid Message received: ", err)
|
||||||
|
}
|
||||||
|
go func(request Request) {
|
||||||
|
var response Response
|
||||||
|
switch request.Request {
|
||||||
|
case Query:
|
||||||
|
response.Actions = actionStore.store.ToReadable()
|
||||||
|
case Flush:
|
||||||
|
actionStore.Flush(request.Pattern)
|
||||||
|
default:
|
||||||
|
log.Println("WARN Invalid Message: unrecognised Request type")
|
||||||
|
}
|
||||||
|
Respond(request, response)
|
||||||
|
}(request)
|
||||||
|
}
|
||||||
|
}
|
@ -24,10 +24,10 @@ func cmdStdout(commandline []string) chan *string {
|
|||||||
cmd := exec.Command(commandline[0], commandline[1:]...)
|
cmd := exec.Command(commandline[0], commandline[1:]...)
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("couldn't open stdout on command:", err)
|
log.Fatalln("couldn't open stdout on command:", err)
|
||||||
}
|
}
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Fatal("couldn't start command:", err)
|
log.Fatalln("couldn't start command:", err)
|
||||||
}
|
}
|
||||||
defer stdout.Close()
|
defer stdout.Close()
|
||||||
|
|
||||||
@ -77,14 +77,17 @@ func sleep(d time.Duration) chan bool {
|
|||||||
func (a *Action) exec(match string, advance time.Duration) {
|
func (a *Action) exec(match string, advance time.Duration) {
|
||||||
defer wgActions.Done()
|
defer wgActions.Done()
|
||||||
|
|
||||||
// Wait for either end of sleep time, or stopActions channel being closed
|
// Wait for either end of sleep time, or actionStore requesting stop
|
||||||
if a.afterDuration != 0 && a.afterDuration > advance {
|
if a.afterDuration != 0 && a.afterDuration > advance {
|
||||||
|
stopAction := actionStore.Register(a, match)
|
||||||
select {
|
select {
|
||||||
case <-sleep(a.afterDuration - advance):
|
case <-sleep(a.afterDuration - advance):
|
||||||
// no-op
|
// no-op
|
||||||
case _, _ = <-stopActions:
|
case _, _ = <-stopAction:
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
// Let's not wait for the lock
|
||||||
|
go actionStore.Unregister(a, match, stopAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
computedCommand := make([]string, 0, len(a.Cmd))
|
computedCommand := make([]string, 0, len(a.Cmd))
|
||||||
@ -174,7 +177,7 @@ func (s *Stream) handle(endedSignal chan *Stream) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var stopStreams chan bool
|
var stopStreams chan bool
|
||||||
var stopActions chan bool
|
var actionStore ActionStore
|
||||||
var wgActions sync.WaitGroup
|
var wgActions sync.WaitGroup
|
||||||
|
|
||||||
var db *gob.Encoder
|
var db *gob.Encoder
|
||||||
@ -188,6 +191,8 @@ func Main() {
|
|||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actionStore.store = make(ActionMap)
|
||||||
|
|
||||||
conf := parseConf(*confFilename)
|
conf := parseConf(*confFilename)
|
||||||
db = conf.updateFromDB()
|
db = conf.updateFromDB()
|
||||||
|
|
||||||
@ -197,7 +202,6 @@ func Main() {
|
|||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
stopStreams = make(chan bool)
|
stopStreams = make(chan bool)
|
||||||
stopActions = make(chan bool)
|
|
||||||
|
|
||||||
endSignals := make(chan *Stream)
|
endSignals := make(chan *Stream)
|
||||||
noStreamsInExecution := len(conf.Streams)
|
noStreamsInExecution := len(conf.Streams)
|
||||||
@ -206,6 +210,8 @@ func Main() {
|
|||||||
go stream.handle(endSignals)
|
go stream.handle(endSignals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go Serve()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case finishedStream := <-endSignals:
|
case finishedStream := <-endSignals:
|
||||||
@ -225,9 +231,11 @@ func quit() {
|
|||||||
// stop all streams
|
// stop all streams
|
||||||
close(stopStreams)
|
close(stopStreams)
|
||||||
// stop all actions
|
// stop all actions
|
||||||
close(stopActions)
|
actionStore.Quit()
|
||||||
// wait for them to complete
|
// wait for them to complete
|
||||||
wgActions.Wait()
|
wgActions.Wait()
|
||||||
|
// delete pipe
|
||||||
|
os.Remove(PipePath())
|
||||||
|
|
||||||
os.Exit(3)
|
os.Exit(3)
|
||||||
}
|
}
|
||||||
|
@ -248,7 +248,7 @@ func (c *Conf) updateFromDB() *gob.Encoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseConf(filename string) (*Conf, *gob.Encoder) {
|
func parseConf(filename string) *Conf {
|
||||||
|
|
||||||
data, err := os.ReadFile(filename)
|
data, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
9
cli.go
Normal file
9
cli.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reaction/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app.CLI()
|
||||||
|
}
|
@ -4,7 +4,7 @@ patterns:
|
|||||||
|
|
||||||
streams:
|
streams:
|
||||||
tailDown:
|
tailDown:
|
||||||
cmd: [ "sh", "-c", "echo 'found 1.1.1.1' && sleep 2s && echo 'found 1.1.1.2' && sleep 2s && echo 'found 1.1.1.1' && sleep 1s" ]
|
cmd: [ "sh", "-c", "echo 'found 1.1.1.1' && sleep 2s && echo 'found 1.1.1.2' && sleep 2s && echo 'found 1.1.1.1' && sleep 10m" ]
|
||||||
filters:
|
filters:
|
||||||
findIP:
|
findIP:
|
||||||
regex:
|
regex:
|
||||||
@ -16,4 +16,4 @@ streams:
|
|||||||
cmd: [ "echo", "<ip>" ]
|
cmd: [ "echo", "<ip>" ]
|
||||||
sleepdamn:
|
sleepdamn:
|
||||||
cmd: [ "echo", "sleep", "<ip>" ]
|
cmd: [ "echo", "sleep", "<ip>" ]
|
||||||
after: 10s
|
after: 8m
|
||||||
|
Loading…
Reference in New Issue
Block a user