From e34f699baaa5dc7c76e6c3e7fde8f16139f516f4 Mon Sep 17 00:00:00 2001 From: boyska Date: Mon, 15 May 2017 11:51:50 +0200 Subject: [PATCH] a first scratch of liquidsoap library --- cmd/direttoforo/main.go | 65 ++++++++++++++++++++++++ liquidsoap/parser.go | 45 +++++++++++++++++ liquidsoap/parser_test.go | 36 +++++++++++++ liquidsoap/spawn.go | 57 +++++++++++++++++++++ liquidsoap/telnet.go | 104 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 307 insertions(+) create mode 100644 cmd/direttoforo/main.go create mode 100644 liquidsoap/parser.go create mode 100644 liquidsoap/parser_test.go create mode 100644 liquidsoap/spawn.go create mode 100644 liquidsoap/telnet.go diff --git a/cmd/direttoforo/main.go b/cmd/direttoforo/main.go new file mode 100644 index 0000000..2f7c855 --- /dev/null +++ b/cmd/direttoforo/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "git.lattuga.net/boyska/direttoforo.git/liquidsoap" + "os" + "os/signal" + "time" +) + +func outUI(output chan liquidsoap.Output) { + for msg := range output { + if msg.Level <= 2 { + fmt.Println(msg) + } + } +} + +func main() { + killLs := make(chan struct{}) // when it is closed, liquidsoap will die + killed := make(chan os.Signal, 1) + signal.Notify(killed, os.Interrupt) // ctrl-c + output, exit, err := liquidsoap.RunLiquidsoap("foo.liq", killLs) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + go outUI(output) + go func() { + tick := time.Tick(3 * time.Second) + for { + <-tick + t, err := liquidsoap.NewTelnet("localhost", 1234) + if err != nil { + fmt.Println("telnet connection errored", err) + continue + } + t.Conn.SetDeadline(time.Now().Add(3 * time.Second)) + out, err := t.Outputs() + if err != nil { + fmt.Println("telnet cmd errored", err) + continue + } + t.Close() + fmt.Println("list=", out) + } + }() + + for { + select { + case how := <-exit: // liquidsoap exits + if !how.Success() { + fmt.Fprintln(os.Stderr, how.Err) + os.Exit(1) + } + os.Exit(0) + case <-killed: // we receive a SIGINT: ask liquidsoap to die is enough + close(killLs) + fmt.Println("Closed by user interaction, waiting for liquidsoap to exit") + // TODO: schedule a more aggressive SIGKILL if liquidsoap doesn't exit soon + } + } + +} diff --git a/liquidsoap/parser.go b/liquidsoap/parser.go new file mode 100644 index 0000000..3d09235 --- /dev/null +++ b/liquidsoap/parser.go @@ -0,0 +1,45 @@ +package liquidsoap + +import ( + "bufio" + "errors" + "fmt" + "io" + "regexp" + "strconv" +) + +// Output represents a liquidsoap message in a structured form +type Output struct { + Level int // lower means more important + Msg string + Component string +} + +// Parse needs a reader of the log, and will produce parsed output +func Parse(r io.Reader, parsed chan<- Output) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + out, err := outParseLine(scanner.Text()) + if err == nil { + parsed <- out + } + } +} + +var lineRegex = regexp.MustCompile(`^([0-9]{4}/[0-9]{2}/[0-9]{2}) ([0-9:]{8}) \[([^:\[\]]*):([0-9])\] (.*)$`) + +func outParseLine(line string) (Output, error) { + res := lineRegex.FindStringSubmatch(line) + if res == nil { + return Output{}, errors.New("Invalid line") + } + if len(res) != 6 { + return Output{}, errors.New("Invalid line: missing parts") + } + level, err := strconv.Atoi(res[4]) + if err != nil { + return Output{}, fmt.Errorf("Invalid line: invalid level `%s`", res[4]) + } + return Output{Level: level, Msg: res[5], Component: res[3]}, nil +} diff --git a/liquidsoap/parser_test.go b/liquidsoap/parser_test.go new file mode 100644 index 0000000..5dfa4f1 --- /dev/null +++ b/liquidsoap/parser_test.go @@ -0,0 +1,36 @@ +package liquidsoap + +import ( + "testing" +) + +type lineParseTest struct { + Line string + Err error + Out Output +} + +var lines = []lineParseTest{ + {"2014/10/13 02:58:16 [/foo(dot)mp3:2] Connection failed: 401", + nil, Output{Level: 2, Msg: "Connection failed: 401", Component: "/foo(dot)mp3"}}, + {"2014/10/13 02:58:16 [asd:1] Gulp[every,strange,char!]", + nil, Output{Level: 1, Msg: "Gulp[every,strange,char!]", Component: "asd"}}, +} + +func TestLine(t *testing.T) { + for _, pair := range lines { + out, err := outParseLine(pair.Line) + if err != pair.Err { + t.Error( + "For", pair.Line, + "error expected", pair.Err, + "got", err) + } + if out != pair.Out { + t.Error( + "For", pair.Line, + "expected", pair.Out, + "got", out) + } + } +} diff --git a/liquidsoap/spawn.go b/liquidsoap/spawn.go new file mode 100644 index 0000000..3b19ea5 --- /dev/null +++ b/liquidsoap/spawn.go @@ -0,0 +1,57 @@ +package liquidsoap + +import ( + "os" + "os/exec" + "syscall" +) + +// End describes how liquidsoap ended; successfully? With errors? +type End struct { + Retval int + Err error +} + +// Success returns true if the process ended successfully +func (e *End) Success() bool { + if e.Err == nil && e.Retval == 0 { + return true + } + return false +} + +// RunLiquidsoap will launch a new instance of liquidsoap and take control of it. It will also read output +// messages and send them to out chan. No magic will be done to liquidsoap config file, so if you want output +// messages, don't forget to set("log.stderr", true) in your liquidsoap file. +// +// RunLiquidsoap is an async function, which provides channels as feedback and needs a channel to ask for its +// termination +func RunLiquidsoap(configfile string, kill <-chan struct{}) (chan Output, chan End, error) { + out := make(chan Output) + exit := make(chan End, 1) + cmd := exec.Command("liquidsoap", "--enable-telnet", configfile) + cmd.Stderr = os.Stderr // connect liquidsoap err to process stderr + log, err := cmd.StdoutPipe() + if err != nil { + return nil, nil, err + } + go Parse(log, out) + err = cmd.Start() + proc := cmd.Process + go func() { + <-kill + proc.Signal(syscall.SIGINT) + }() + go func() { + defer log.Close() + err = cmd.Wait() + close(out) + if err != nil { + exit <- End{Err: err} + } else { + exit <- End{} + } + close(exit) + }() + return out, exit, err +} diff --git a/liquidsoap/telnet.go b/liquidsoap/telnet.go new file mode 100644 index 0000000..31ca301 --- /dev/null +++ b/liquidsoap/telnet.go @@ -0,0 +1,104 @@ +package liquidsoap + +import ( + "bufio" + "fmt" + "net" + "strings" +) + +// Client represents a telnet/unix domain connection to Liquidsoap +// +// A Client can perform low-level operations, such as Command(), or high-level operations such as Outputs() +type Client struct { + Conn net.Conn +} + +// NewTelnet returns a liquidsoap.Client created using telnet on the given parameters +func NewTelnet(host string, port int) (Client, error) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) + if err != nil { + return Client{}, err + } + return Client{Conn: conn}, nil +} + +// Close closes the connection +func (c *Client) Close() { + if c.Conn != nil { + c.Conn.Write([]byte("quit\n")) + c.Conn.Close() + } +} + +// Command run a command, wait for output response to come and returns it +func (c *Client) Command(cmdline string) (string, error) { + c.Conn.Write([]byte(cmdline + "\n")) + var out, line string + var err error + reader := bufio.NewReader(c.Conn) + for { + if line, err = reader.ReadString('\n'); err != nil { + return "", err + } + line = line[:len(line)-2] + "\n" // \r\n -> \n + if line == "END\n" { + return out, nil + } + out += line + } +} + +// Outputs will return a map of outputs that liquidsoap is handling. +// An output is set to true if it is enabled +func (c *Client) Outputs() (outputs map[string]bool, err error) { + outputs = make(map[string]bool) + var cmdout string + cmdout, err = c.Command("list") + if err != nil { + return + } + lines := strings.Split(cmdout, "\n") + for _, l := range lines { + parts := strings.SplitN(l, " : ", 2) + if len(parts) < 2 { + continue + } + if strings.Index(parts[1], "output.") == 0 { + name := strings.TrimSpace(parts[0]) + var enabled bool + enabled, err = c.GetOutput(name) + if err != nil { + return + } + outputs[name] = enabled + } + } + + return +} + +// GetOutput checks whether an output is enabled +func (c *Client) GetOutput(name string) (bool, error) { + cmdout, err := c.Command(name + ".status") + if err != nil { + return false, err + } + return strings.TrimSpace(cmdout) == "on", nil +} + +// SetOutput enables or disables an output +func (c *Client) SetOutput(name string, enabled bool) error { + action := "stop" + if enabled { + action = "start" + } + cmdout, err := c.Command(fmt.Sprintf("%s.%s", name, action)) + if err != nil { + return err + } + if strings.TrimSpace(cmdout) != "OK" { + return fmt.Errorf("Error: liquidsoap replied '%s'", cmdout) + } + return nil +}