a first scratch of liquidsoap library

This commit is contained in:
boyska 2017-05-15 11:51:50 +02:00
commit e34f699baa
5 changed files with 307 additions and 0 deletions

65
cmd/direttoforo/main.go Normal file
View file

@ -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
}
}
}

45
liquidsoap/parser.go Normal file
View file

@ -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
}

36
liquidsoap/parser_test.go Normal file
View file

@ -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)
}
}
}

57
liquidsoap/spawn.go Normal file
View file

@ -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
}

104
liquidsoap/telnet.go Normal file
View file

@ -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
}