Files
go-sip/internal/sipcall/native.go
T

105 lines
3.2 KiB
Go

package sipcall
import (
"context"
"errors"
"fmt"
"git.ipao.vip/rogee/go-sip/internal/asterisk"
"github.com/CyCoreSystems/ari/v5"
"golang.org/x/sys/unix"
"os"
"os/exec"
"path/filepath"
"regexp"
"time"
)
type nativeHost struct {
config Config
client ari.Client
lock *os.File
}
var activeChannels = regexp.MustCompile(`(?m)^([0-9]+) active channels?\s*$`)
func (h *nativeHost) CheckIdle(ctx context.Context) error {
if h.lock == nil {
f, err := os.OpenFile(filepath.Join(h.config.ConfigDir, ".sip-call.lock"), os.O_CREATE|os.O_RDWR|unix.O_NOFOLLOW, 0600)
if err != nil {
return fmt.Errorf("open test lock: %w", err)
}
if err = unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
return errors.Join(errors.New("another sip-call owns this Asterisk configuration"), f.Close())
}
h.lock = f
}
data, err := asteriskCLI(ctx, h.config, "core show channels count")
if err != nil {
return err
}
m := activeChannels.FindSubmatch(data)
if len(m) != 2 || string(m[1]) != "0" {
return errors.New("Asterisk idle state unconfirmed or active channels exist; no configuration changes or dialing allowed")
}
if h.client == nil {
h.client, err = (asterisk.Loader{ConfigDir: h.config.ConfigDir}).OpenLineTestARI()
if err != nil {
return err
}
}
apps, err := h.client.Application().List(nil)
if err != nil {
return fmt.Errorf("read native ARI applications: %w", err)
}
for _, app := range apps {
if app == nil || app.ID != "sip-call" {
return errors.New("another ARI application is active; stop business services before the isolated line test")
}
}
return nil
}
func (h *nativeHost) Apply(ctx context.Context, c Config) (func() error, error) {
h.config = c
return applyConfiguration(ctx, c, func(ctx context.Context, s string) ([]byte, error) { return asteriskCLI(ctx, c, s) })
}
func (h *nativeHost) Dial(ctx context.Context, request asterisk.NativeDial) (testCall, error) {
call, err := asterisk.DialLineTest(ctx, h.client, request)
if err != nil {
return nil, err
}
return &nativeTestCall{call}, nil
}
func (h *nativeHost) Close() error {
if h.client != nil {
h.client.Close()
}
if h.lock != nil {
return errors.Join(unix.Flock(int(h.lock.Fd()), unix.LOCK_UN), h.lock.Close())
}
return nil
}
type nativeTestCall struct{ *asterisk.NativeCall }
func (c *nativeTestCall) Done() context.Context { return c.Context }
func asteriskCLI(ctx context.Context, c Config, command string) ([]byte, error) {
bounded, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
cmd := exec.CommandContext(bounded, c.Binary, "-C", filepath.Join(c.ConfigDir, "asterisk.conf"), "-rx", command)
cmd.Env = append(os.Environ(), "LD_LIBRARY_PATH="+c.LibraryDir)
out, err := cmd.CombinedOutput()
if c.resultDir != "" {
log, logErr := os.OpenFile(filepath.Join(c.resultDir, "asterisk-cli.log"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
if logErr == nil {
_, logErr = fmt.Fprintf(log, "[%s] %s\n%s\n", time.Now().UTC().Format(time.RFC3339Nano), command, out)
logErr = errors.Join(logErr, log.Close())
}
err = errors.Join(err, logErr)
}
if err != nil {
return nil, fmt.Errorf("Asterisk command %q failed: %w", command, err)
}
return out, nil
}