use crate::proto::{
self, AgentConfig, Command, FromWorker, Query, QueryEnvelope, ReplyResult, ToWorker,
WireSavedAudio,
};
use anyhow::{Context, Result};
use ringo_core::AudioFrame;
use ringo_core::event::{AppEvent, InviteHeaders, MediaStats};
use std::collections::HashMap;
use std::io::BufReader;
use std::process::{Child, ChildStdin, Command as OsCommand, Stdio};
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
const QUERY_TIMEOUT: Duration = Duration::from_secs(10);
const READY_TIMEOUT: Duration = Duration::from_secs(10);
const SHUTDOWN_GRACE: Duration = Duration::from_secs(5);
#[derive(Debug, Clone)]
pub struct SavedAudio {
pub path: String,
pub kind: String,
pub duration_ms: u64,
pub samples: usize,
pub srate: u32,
pub truncated: bool,
}
impl From<WireSavedAudio> for SavedAudio {
fn from(w: WireSavedAudio) -> Self {
let duration_ms = w.samples as u64 * 1000 / w.srate.max(1) as u64;
Self {
path: w.path,
kind: w.kind,
duration_ms,
samples: w.samples,
srate: w.srate,
truncated: w.truncated,
}
}
}
pub struct ProcessClient {
name: String,
pid: u32,
stdin: Mutex<ChildStdin>,
pending: Arc<Mutex<HashMap<u64, Sender<ReplyResult>>>>,
next_id: AtomicU64,
headers: Arc<Mutex<Option<InviteHeaders>>>,
child: Mutex<Option<Child>>,
shutdown_sent: AtomicBool,
dead: Arc<AtomicBool>,
rx_audio: Arc<Mutex<Option<Sender<AudioFrame>>>>,
}
impl ProcessClient {
pub fn spawn(config: AgentConfig) -> Result<(Self, Receiver<AppEvent>)> {
let exe = std::env::current_exe().context("locate own executable")?;
let mut os = OsCommand::new(exe);
os.arg("agent")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
#[cfg(target_os = "linux")]
{
use std::os::unix::process::CommandExt;
unsafe {
os.pre_exec(|| {
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM as libc::c_ulong) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::getppid() == 1 {
libc::raise(libc::SIGTERM);
}
Ok(())
});
}
}
let mut child = os
.spawn()
.with_context(|| format!("spawn agent worker for `{}`", config.name))?;
let pid = child.id();
let mut stdin = child.stdin.take().context("worker stdin missing")?;
let stdout = child.stdout.take().context("worker stdout missing")?;
let handshake = proto::Handshake {
proto_version: proto::PROTO_VERSION,
config,
};
let cfg_bytes = serde_json::to_vec(&handshake).context("serialize handshake")?;
proto::write_frame(&mut stdin, proto::FRAME_CONTROL, &cfg_bytes)
.context("write config handshake")?;
let (ev_tx, ev_rx) = channel::<AppEvent>();
let pending: Arc<Mutex<HashMap<u64, Sender<ReplyResult>>>> = Arc::default();
let headers: Arc<Mutex<Option<InviteHeaders>>> = Arc::default();
let dead = Arc::new(AtomicBool::new(false));
let rx_audio: Arc<Mutex<Option<Sender<AudioFrame>>>> = Arc::default();
let rx_audio_rate = Arc::new(AtomicU32::new(0));
let (ready_tx, ready_rx) = channel::<()>();
let r_pending = Arc::clone(&pending);
let r_headers = Arc::clone(&headers);
let r_dead = Arc::clone(&dead);
let r_rx_audio = Arc::clone(&rx_audio);
let r_rx_rate = Arc::clone(&rx_audio_rate);
let label = handshake.config.name.clone();
let mut reader = BufReader::new(stdout);
std::thread::spawn(move || {
loop {
let (kind, payload) = match proto::read_frame(&mut reader) {
Ok(Some(f)) => f,
Ok(None) => break, Err(e) => {
ringo_core::rlog!(Warn, "agent `{label}`: read error: {e}");
break;
}
};
if kind == proto::FRAME_AUDIO {
let sink = r_rx_audio.lock().unwrap_or_else(|e| e.into_inner());
if let Some(tx) = sink.as_ref() {
let frame = AudioFrame {
samples: proto::bytes_to_pcm(&payload),
rate: r_rx_rate.load(Ordering::Relaxed),
};
let _ = tx.send(frame);
}
continue;
}
if kind != proto::FRAME_CONTROL {
continue;
}
match serde_json::from_slice::<FromWorker>(&payload) {
Ok(FromWorker::Ready(_)) => {
let _ = ready_tx.send(());
}
Ok(FromWorker::Event(w)) => {
if ev_tx.send(w.into()).is_err() {
break; }
}
Ok(FromWorker::Reply(r)) => {
if let Some(tx) = r_pending
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&r.reply)
{
let _ = tx.send(r.result);
}
}
Ok(FromWorker::Headers(h)) => {
let mut buf = r_headers.lock().unwrap_or_else(|e| e.into_inner());
match buf.as_mut() {
Some(existing) => existing.extend(h.headers),
None => *buf = Some(h.headers),
}
}
Ok(FromWorker::RxAudioStarted(s)) => {
r_rx_rate.store(s.rx_audio_rate, Ordering::Relaxed);
}
Err(e) => ringo_core::rlog!(
Warn,
"agent `{label}`: bad control frame ({} bytes): {e}",
payload.len()
),
}
}
let mut pending = r_pending.lock().unwrap_or_else(|e| e.into_inner());
r_dead.store(true, Ordering::Relaxed);
pending.clear();
});
match ready_rx.recv_timeout(READY_TIMEOUT) {
Ok(()) => {}
Err(_) => {
let _ = child.kill();
let _ = child.wait();
anyhow::bail!(
"agent `{}` not ready within {:?} (bad config / failed backend / exited)",
handshake.config.name,
READY_TIMEOUT
);
}
}
Ok((
Self {
name: handshake.config.name,
pid,
stdin: Mutex::new(stdin),
pending,
next_id: AtomicU64::new(0),
headers,
child: Mutex::new(Some(child)),
shutdown_sent: AtomicBool::new(false),
dead,
rx_audio,
},
ev_rx,
))
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn headers_handle(&self) -> Arc<Mutex<Option<InviteHeaders>>> {
Arc::clone(&self.headers)
}
fn send(&self, msg: ToWorker) {
let bytes = match serde_json::to_vec(&msg) {
Ok(b) => b,
Err(e) => {
ringo_core::rlog!(Warn, "agent `{}`: serialize message: {e}", self.name);
return;
}
};
let mut w = self.stdin.lock().unwrap_or_else(|e| e.into_inner());
if proto::write_frame(&mut *w, proto::FRAME_CONTROL, &bytes).is_err() {
ringo_core::rlog!(Warn, "agent `{}`: worker stdin closed", self.name);
}
}
fn cmd(&self, cmd: Command) {
self.send(ToWorker::Cmd(cmd));
}
pub fn request_shutdown(&self) {
if !self.shutdown_sent.swap(true, Ordering::Relaxed) {
self.cmd(Command::Shutdown);
}
}
fn query(&self, query: Query) -> Option<ReplyResult> {
if self.dead.load(Ordering::Relaxed) {
return None;
}
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = channel::<ReplyResult>();
{
let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
if self.dead.load(Ordering::Relaxed) {
return None;
}
pending.insert(id, tx);
}
self.send(ToWorker::Query(QueryEnvelope { id, query }));
match rx.recv_timeout(QUERY_TIMEOUT) {
Ok(r) => Some(r),
Err(_) => {
self.pending
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&id);
ringo_core::rlog!(Warn, "agent `{}`: query timed out", self.name);
None
}
}
}
pub fn register(&self, aor: &str, regint: u32) {
self.cmd(Command::Register {
aor: aor.to_string(),
regint,
});
}
pub fn dial(&self, number: &str) {
self.cmd(Command::Dial {
number: number.to_string(),
});
}
pub fn hangup(&self) {
self.cmd(Command::Hangup);
}
pub fn hangup_all(&self) {
self.cmd(Command::HangupAll);
}
pub fn accept(&self) {
self.cmd(Command::Accept);
}
pub fn hold(&self) {
self.cmd(Command::Hold);
}
pub fn resume(&self) {
self.cmd(Command::Resume);
}
pub fn mute(&self) {
self.cmd(Command::Mute);
}
pub fn send_dtmf(&self, digit: char) {
self.cmd(Command::Dtmf { digit });
}
#[allow(dead_code)]
pub fn switch_line(&self, line: usize) {
self.cmd(Command::SwitchLine { line });
}
pub fn transfer(&self, uri: &str) {
self.cmd(Command::Transfer {
uri: uri.to_string(),
});
}
pub fn attended_transfer_start(&self, uri: &str) {
self.cmd(Command::AttendedTransferStart {
uri: uri.to_string(),
});
}
pub fn attended_transfer_exec(&self) {
self.cmd(Command::AttendedTransferExec);
}
pub fn attended_transfer_abort(&self) {
self.cmd(Command::AttendedTransferAbort);
}
pub fn add_header(&self, key: &str, value: &str) {
self.cmd(Command::AddHeader {
key: key.to_string(),
value: value.to_string(),
});
}
pub fn rm_header(&self, key: &str) {
self.cmd(Command::RmHeader {
key: key.to_string(),
});
}
pub fn set_audio_source(&self, spec: &str) {
self.cmd(Command::SetAudioSource {
spec: spec.to_string(),
});
}
pub fn set_recording(&self, on: bool) {
self.cmd(Command::SetRecording { on });
}
pub fn arm_invite_response(&self, scode: u16, reason: &str, headers: Vec<String>) {
self.cmd(Command::ArmInviteResponse {
scode,
reason: reason.to_string(),
headers,
});
}
pub fn disarm_invite_response(&self) {
self.cmd(Command::DisarmInviteResponse);
}
pub fn deflect_incoming(&self, contact: &str, diversion: Option<&str>) {
let mut headers = vec![format!("Contact: <{contact}>")];
if let Some(div) = diversion {
headers.push(format!("Diversion: <{div}>"));
}
self.arm_invite_response(302, "Moved Temporarily", headers);
}
pub fn media_stats(&self) -> Option<MediaStats> {
match self.query(Query::MediaStats) {
Some(ReplyResult::MediaStats(s)) => s.map(Into::into),
_ => None,
}
}
pub fn received_dtmf(&self) -> String {
match self.query(Query::ReceivedDtmf) {
Some(ReplyResult::Dtmf(s)) => s,
_ => String::new(),
}
}
pub fn save_audio(&self, prefix: &str) -> Vec<String> {
self.save_audio_detailed(prefix)
.into_iter()
.map(|f| f.path)
.collect()
}
pub fn save_audio_detailed(&self, prefix: &str) -> Vec<SavedAudio> {
match self.query(Query::SaveAudio {
prefix: prefix.to_string(),
}) {
Some(ReplyResult::SavedAudio(files)) => {
files.into_iter().map(SavedAudio::from).collect()
}
_ => Vec::new(),
}
}
pub fn call_count(&self) -> u32 {
match self.query(Query::CallCount) {
Some(ReplyResult::CallCount(n)) => n,
_ => 0,
}
}
pub fn start_tx_audio(&self, rate: u32) {
self.cmd(Command::StartTxAudio { rate });
}
pub fn push_tx_audio(&self, samples: &[i16]) {
let mut w = self.stdin.lock().unwrap_or_else(|e| e.into_inner());
if proto::write_frame(&mut *w, proto::FRAME_AUDIO, &proto::pcm_to_bytes(samples)).is_err() {
ringo_core::rlog!(
Warn,
"agent `{}`: worker stdin closed (tx audio)",
self.name
);
}
}
pub fn start_rx_audio(&self) -> Receiver<AudioFrame> {
let (tx, rx) = channel();
*self.rx_audio.lock().unwrap_or_else(|e| e.into_inner()) = Some(tx);
self.cmd(Command::StartRxAudio);
rx
}
}
impl Drop for ProcessClient {
fn drop(&mut self) {
self.request_shutdown();
if let Some(mut child) = self.child.lock().unwrap_or_else(|e| e.into_inner()).take() {
let deadline = Instant::now() + SHUTDOWN_GRACE;
loop {
match child.try_wait() {
Ok(Some(_)) => return, Ok(None) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(20))
}
_ => break, }
}
let _ = child.kill();
let _ = child.wait();
}
}
}