fix(sip-call): require native PJSIP module reload confirmation
This commit is contained in:
@@ -107,7 +107,7 @@
|
||||
| 线路 | SIP 服务端(历史登记,非已验证线路) | 主叫标识 | 被叫前缀 |
|
||||
| --- | --- | --- | --- |
|
||||
| 数企 | `61.132.228.221:5060` | `BD93205882` | `7089` |
|
||||
| 中鼎 | `60.171.24.90:5060` | `mbkq` | 无 |
|
||||
| 中鼎 | `222.186.130.228:5060` | `mbkq` | 无 |
|
||||
| 百应 | `160.202.254.79:5060` | `KQ91526` | `mka755108` |
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ chmod 600 sip-xx.env
|
||||
|
||||
## 自动配置与退出
|
||||
|
||||
取得该 Asterisk 配置目录的独占测试锁,确认空闲后保存原 `pjsip.conf` 到本次私有目录的 `pjsip.conf.before`。临时增加独立 include/endpoint/AOR,reload 后核对实际服务地址、主叫、codec、transport 和 context。通过才执行一次原生 ARI Dial。
|
||||
取得该 Asterisk 配置目录的独占测试锁,确认空闲后保存原 `pjsip.conf` 到本次私有目录的 `pjsip.conf.before`。临时增加独立 include/endpoint/AOR,使用原生 `module reload res_pjsip.so` 并核对明确的成功响应,不依赖可选的 `pjsip reload` 别名,不把 CLI 退出码 0 当作加载成功;随后核对实际服务地址、主叫、codec、transport 和 context。通过才执行一次原生 ARI Dial。
|
||||
|
||||
收到真正的 `Up` 和 `StasisStart` 才记录接通;按 `HOLD_SECONDS` 保持后挂断。观察到通道结束或挂断后的 ARI 404 才记录结束确认。正常结束后恢复原 `pjsip.conf` 原字节、删除本次专用配置、reload 并确认 endpoint 已移除,不改 `go-sip-managed.conf` 或业务 SIP 加载代次。
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package sipcall
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -88,7 +89,7 @@ func applyConfiguration(ctx context.Context, c Config, cli cliFunc) (restore fun
|
||||
if e := os.Remove(managed); e != nil {
|
||||
return e
|
||||
}
|
||||
if _, e := cli(cleanupCtx, "pjsip reload"); e != nil {
|
||||
if e := reloadPJSIP(cleanupCtx, cli); e != nil {
|
||||
return e
|
||||
}
|
||||
out, e := cli(cleanupCtx, "pjsip show endpoint "+c.Endpoint())
|
||||
@@ -106,7 +107,7 @@ func applyConfiguration(ctx context.Context, c Config, cli cliFunc) (restore fun
|
||||
if err = atomicConfig(basePath, appended); err != nil {
|
||||
return restore, err
|
||||
}
|
||||
if _, err = cli(ctx, "pjsip reload"); err != nil {
|
||||
if err = reloadPJSIP(ctx, cli); err != nil {
|
||||
return restore, err
|
||||
}
|
||||
endpoint, err := cli(ctx, "pjsip show endpoint "+c.Endpoint())
|
||||
@@ -140,6 +141,21 @@ func applyConfiguration(ctx context.Context, c Config, cli cliFunc) (restore fun
|
||||
return restore, nil
|
||||
}
|
||||
|
||||
// Asterisk CLI errors can exit zero. Confirm the native module response before
|
||||
// checking loaded objects; pjsip reload is an optional alias, not a native command.
|
||||
func reloadPJSIP(ctx context.Context, cli cliFunc) error {
|
||||
out, err := cli(ctx, "module reload res_pjsip.so")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if strings.TrimSpace(line) == "Module 'res_pjsip.so' reloaded successfully." {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("native PJSIP reload not confirmed (output_sha256=%x)", sha256.Sum256(out))
|
||||
}
|
||||
|
||||
func atomicConfig(path string, data []byte) error {
|
||||
f, err := os.CreateTemp(filepath.Dir(path), ".sip-call-write-*")
|
||||
if err != nil {
|
||||
|
||||
@@ -18,8 +18,8 @@ func configCLI(c Config, basePath string) cliFunc {
|
||||
return []byte("0 active channels\n0 active calls\n"), nil
|
||||
case "pjsip show transports":
|
||||
return []byte("Transport: go-sip-udp udp 0 0 0.0.0.0:5060\n"), nil
|
||||
case "pjsip reload":
|
||||
return []byte("Module reloaded\n"), nil
|
||||
case "module reload res_pjsip.so":
|
||||
return []byte("Module 'res_pjsip.so' reloaded successfully.\n"), nil
|
||||
case "pjsip show endpoint " + c.Endpoint():
|
||||
data, err := os.ReadFile(basePath)
|
||||
if err != nil {
|
||||
@@ -116,7 +116,7 @@ func TestConfigurationFailsClosedOnRuntimeMismatch(t *testing.T) {
|
||||
if failure == "caller" && strings.HasPrefix(s, "pjsip show endpoint") {
|
||||
return bytes.ReplaceAll(out, []byte("BD1234"), []byte("WRONG")), nil
|
||||
}
|
||||
if failure == "reload" && s == "pjsip reload" {
|
||||
if failure == "reload" && s == "module reload res_pjsip.so" {
|
||||
return nil, errors.New("reload failed")
|
||||
}
|
||||
if failure == "aor" && strings.HasPrefix(s, "pjsip show aor") {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package sipcall
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNativePJSIPModuleReloadIsUsedAndConfirmed(t *testing.T) {
|
||||
c, path, base := stageFixture(t)
|
||||
fixture := configCLI(c, path)
|
||||
var reloads int
|
||||
cli := func(ctx context.Context, command string) ([]byte, error) {
|
||||
switch command {
|
||||
case "pjsip reload":
|
||||
return []byte("No such command 'pjsip reload' (type 'core show help pjsip reload' for other possible commands)\n"), nil
|
||||
case "module reload res_pjsip.so":
|
||||
reloads++
|
||||
return []byte("Module 'res_pjsip.so' reloaded successfully.\n"), nil
|
||||
default:
|
||||
return fixture(ctx, command)
|
||||
}
|
||||
}
|
||||
restore, err := applyConfiguration(context.Background(), c, cli)
|
||||
if err != nil {
|
||||
t.Fatalf("supported native reload failed: %v", err)
|
||||
}
|
||||
if err := restore(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reloads != 2 {
|
||||
t.Fatalf("expected native module reload for apply and restore, got %d", reloads)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil || !bytes.Equal(data, base) {
|
||||
t.Fatal("original configuration not restored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPJSIPReloadRejectsZeroExitWithoutModuleConfirmation(t *testing.T) {
|
||||
for _, output := range []string{
|
||||
"No such command 'module reload res_pjsip.so'\n",
|
||||
"Module 'res_pjsip.so' could not be reloaded.\n",
|
||||
"Module 'res_other.so' reloaded successfully.\n",
|
||||
"",
|
||||
} {
|
||||
for _, phase := range []string{"apply", "restore"} {
|
||||
t.Run(fmt.Sprintf("%s/%q", phase, output), func(t *testing.T) {
|
||||
c, path, _ := stageFixture(t)
|
||||
fixture := configCLI(c, path)
|
||||
var reloads, runtimeReads int
|
||||
cli := func(ctx context.Context, command string) ([]byte, error) {
|
||||
if command == "module reload res_pjsip.so" {
|
||||
reloads++
|
||||
if phase == "apply" || reloads == 2 {
|
||||
return []byte(output), nil
|
||||
}
|
||||
return []byte("Module 'res_pjsip.so' reloaded successfully.\n"), nil
|
||||
}
|
||||
if strings.HasPrefix(command, "pjsip show endpoint ") {
|
||||
runtimeReads++
|
||||
}
|
||||
return fixture(ctx, command)
|
||||
}
|
||||
restore, err := applyConfiguration(context.Background(), c, cli)
|
||||
if phase == "apply" {
|
||||
if err == nil || !strings.Contains(err.Error(), "reload not confirmed") || runtimeReads != 0 {
|
||||
t.Fatalf("unconfirmed reload must fail before readback: error=%v reads=%d", err, runtimeReads)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = restore(); err == nil || !strings.Contains(err.Error(), "reload not confirmed") {
|
||||
t.Fatalf("unconfirmed restoration reload accepted: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user