/goesl

Primary LanguageGoBSD 2-Clause "Simplified" LicenseBSD-2-Clause

Description

goesl is a go library to connect to FreeSWITCH via event socket, only inbound now.

Installation

Installation can be done as usual:

$ go get github.com/genmzy/goesl

How it works

goesl.NewConnection create a new esl connection and take a ConnHandler interface which defines the callbacks to handle the event-socket events and freeswitch logs.

Example of use

  • A correct way to close connection immediately should like this:
conn, err := conn.Dial(addr, passwd, handler, opts...)
if err != nil {
	// do some error log
	return;
}
defer conn.Close()
ctx, cancel := context.WithCancel(context.Background())
// ...
go conn.HandleEvents(ctx, /* ... other params ... */)

// when time to quit:
if timeToQuit { // triggered should quit
	log.Println("exiting...")
	cancel()
	// close twice is allowed
	conn.Close()
}
  • Here is a fs_cli-like program based on goesl:
package main

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"flag"
	"fmt"
	"net"
	"os"
	"os/signal"
	"regexp"
	"strings"
	"syscall"
	"time"

	"github.com/genmzy/goesl"
)

type Handler struct{}

var (
	host    = flag.String("H", "127.0.0.1", "Specify the host of freeswitch")
	port    = flag.Int("P", 8021, "Specify the port of freeswitch")
	passwd  = flag.String("p", "ClueCon", "Specify the default password of freeswitch")
	level   = flag.String("l", "debug", "Specify the log level of freeswitch")
	logPath = flag.String("log_file", "", "Specify the log path of fs_cli self")
)

func main() {
	flag.Parse()

	go signalCatch()

	opts := make([]goesl.Option, 0)
	opts = append(opts,
		goesl.WithDefaultAutoRedial(),
		goesl.WithHeartBeat(20*time.Second),
		goesl.WithNetDelay(2*time.Second),
		goesl.WithMaxRetries(-1),
		goesl.WithLogLevel(goesl.LevelDebug),
		goesl.WithLogPrefix("<fs_cli>"),
	)

	if *logPath != "" {
		logFile, err := os.OpenFile(*logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
		if err != nil {
			goesl.Warnf("error when open log file %s: %v, use stdout now ...", *logPath, err)
		} else {
			opts = append(opts, goesl.WithLogOutput(logFile))
			defer logFile.Close()
		}
	}

	handler := &Handler{}
	conn, err := goesl.Dial(fmt.Sprintf("%s:%d", *host, *port), *passwd, handler, opts...)
	if err != nil {
		goesl.Fatalf("connecting to freeswitch: %v", err)
	}
	// close twice is allowed
	defer conn.Close()

	ctx, cancel := context.WithCancel(context.Background())
	go messageSend(ctx, conn, cancel)

	err = conn.HandleEvents(ctx)
	if err == nil || errors.Is(err, net.ErrClosed) || errors.Is(err, context.Canceled) {
		goesl.Noticef("process exiting...")
	} else {
		goesl.Errorf("exiting with error: %v", err)
	}
}

type SofiaEntry struct {
	Name   string
	Type   string // gateway or profile
	Url    string
	Status string
	Extra  string
}

func (se SofiaEntry) String() string {
	return fmt.Sprintf(`{"name":"%s","type":"%s","url":"%s","status":"%s","extra":"%s"}`,
		se.Name, se.Type, se.Url, se.Status, se.Extra)
}

// return sofia entry groups
func SofiaStatus(raw []byte) (entries []*SofiaEntry) {
	s := bufio.NewScanner(bytes.NewBuffer(raw))
	start := false
	for s.Scan() {
		t := s.Text()
		if t[0] == '=' { // ====================
			if start { // end line
				break
			} else { // start line
				start = true
				continue
			}
		}
		if !start {
			continue
		}
		// content line now
		t = strings.TrimSpace(t)
		reg := regexp.MustCompile(`\s+`)
		sli := reg.Split(t, -1)
		entry := &SofiaEntry{Name: sli[0], Type: sli[1], Url: sli[2], Status: sli[3]}
		if len(sli) > 4 {
			entry.Extra = sli[4]
		}
		entries = append(entries, entry)
	}
	return
}

func (h *Handler) OnConnect(conn *goesl.Connection) {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	// conn.Plain(ctx, []ev_name.EventName{ev_name.BACKGROUND_JOB, ev_name.API}, nil)
	conn.Fslog(ctx, goesl.FslogString2Level(*level))

	die := func(s string, err error) string {
		if err != nil {
			conn.Close()
			goesl.Fatalf("api: %v", err)
		}
		return s
	}

	core := die(conn.Api(ctx, "global_getvar", "core_uuid"))
	raw := die(conn.Api(ctx, "sofia", "status"))
	entries := SofiaStatus([]byte(raw))
	goesl.Noticef("connect and get freeswitch (core: %s with sofia entries: %v)", core, entries)
}

func (h *Handler) OnDisconnect(conn *goesl.Connection, ev goesl.Event) {
	goesl.Noticef("esl disconnected: %v", ev)
}

func (h *Handler) OnClose(con *goesl.Connection) {
	goesl.Noticef("esl connection closed")
}

func (h *Handler) OnEvent(ctx context.Context, conn *goesl.Connection, e goesl.Event) {
	fmt.Print("\nRECV EVENT\n", e.EventContent())
}

var colorFormats = map[goesl.FslogLevel]string{
	goesl.FslogLevel_DEBUG:   "\033[0;33m%s\033[0m\n",
	goesl.FslogLevel_INFO:    "\033[0;32m%s\033[0m\n",
	goesl.FslogLevel_NOTICE:  "\033[0;36m%s\033[0m\n",
	goesl.FslogLevel_WARNING: "\033[0;35m%s\033[0m\n",
	goesl.FslogLevel_ERROR:   "\033[0;31m%s\033[0m\n",
	goesl.FslogLevel_CRIT:    "\033[0;31m%s\033[0m\n",
	goesl.FslogLevel_ALERT:   "\033[0;31m%s\033[0m\n",
}

func getColorFormat(lv goesl.FslogLevel) string {
	if format, ok := colorFormats[lv]; ok {
		return format
	} else {
		return "%s\n\n"
	}
}

func (h *Handler) OnFslog(ctx context.Context, conn *goesl.Connection, fslog goesl.Fslog) {
	var content string
	if fslog.UserData != "" {
		content = fmt.Sprintf("%s %s", fslog.UserData, fslog.Content)
	} else {
		content = fslog.Content
	}
	fmt.Printf(getColorFormat(fslog.Level), content)
}

func send(ctx context.Context, conn *goesl.Connection, cmd string) {
	check := func(res string, err error) {
		if err != nil {
			goesl.Errorf("api %s error: %v", cmd, err)
			return
		}
		fmt.Println(res)
	}

	if cmd[0] == '/' {
		check(conn.Send(ctx, cmd[1:]))
		return
	}
	if strings.Split(cmd, " ")[0] == "originate" {
		goesl.Noticef("now use bapi originate instead of originate")
		check(conn.BgApi(ctx, cmd))
		return
	}
	check(conn.Api(ctx, cmd))
}

func messageSend(ctx context.Context, conn *goesl.Connection, cancel func()) {
	s := bufio.NewScanner(os.Stdin)
	host, err := conn.Api(ctx, "global_getvar", "hostname")
	if err != nil {
		goesl.Errorf("api global_getvar hostname send: %v", err)
		return
	}
	cliPrefix := fmt.Sprintf("freeswitch/%s> ", host)
	fmt.Print(cliPrefix)
	for s.Scan() {
		fmt.Print(cliPrefix)
		switch cmd := s.Text(); cmd {
		case "":
		case "...", "/exit", "/bye", "/quit":
			cancel()
			// close twice is allowed
			conn.Close()
			return
		default:
			send(ctx, conn, cmd)
		}
	}
}

func signalCatch() {
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	defer signal.Stop(sigs)
	<-sigs
	// need to set environment `GOTRACEBACK=1`
	panic("show all goroutines")
}

TODO

  • add documentation
  • add tests
  • more usage examples