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 }