Browse Source

a first scratch of liquidsoap library

boyska 7 years ago
commit
e34f699baa
5 changed files with 307 additions and 0 deletions
  1. 65 0
      cmd/direttoforo/main.go
  2. 45 0
      liquidsoap/parser.go
  3. 36 0
      liquidsoap/parser_test.go
  4. 57 0
      liquidsoap/spawn.go
  5. 104 0
      liquidsoap/telnet.go

+ 65 - 0
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
+		}
+	}
+
+}

+ 45 - 0
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
+}

+ 36 - 0
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)
+		}
+	}
+}

+ 57 - 0
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
+}

+ 104 - 0
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
+}