1use crate::audio;
15use crate::proto::{
16 self, Command, FromWorker, Headers, Query, QueryEnvelope, Ready, Reply, ReplyResult,
17 RxAudioStarted, ToWorker, WireEvent, WireMediaStats,
18};
19use anyhow::{Context, Result, bail};
20use ringo_core::backend::{Backend, BaresipBackend};
21use ringo_core::phone::Phone;
22use std::sync::Arc;
23use std::sync::atomic::{AtomicBool, Ordering};
24use std::sync::mpsc::{RecvTimeoutError, SyncSender};
25use std::thread::JoinHandle;
26use std::time::Duration;
27
28enum Out {
31 Control(FromWorker),
32 Audio(Vec<i16>),
33}
34
35const HEADER_POLL_INTERVAL: Duration = Duration::from_millis(150);
37
38const WRITER_CHANNEL_BOUND: usize = 1024;
42
43pub fn run() -> Result<()> {
47 let stdin = std::io::stdin();
48 let mut stdin = stdin.lock();
49
50 let handshake: proto::Handshake = match proto::read_frame(&mut stdin) {
53 Ok(Some((proto::FRAME_CONTROL, payload))) => {
54 serde_json::from_slice(&payload).context("parse config handshake")?
55 }
56 Ok(Some(_)) => bail!("worker: first frame was not the config handshake"),
57 Ok(None) => bail!("worker stdin closed before config handshake"),
58 Err(e) => return Err(e).context("read config handshake"),
59 };
60 if handshake.proto_version != proto::PROTO_VERSION {
61 bail!(
62 "worker: protocol version mismatch (parent {}, worker {}); rebuild so \
63 both sides share one binary",
64 handshake.proto_version,
65 proto::PROTO_VERSION
66 );
67 }
68 let config = handshake.config;
69
70 init_logging(&config.name);
73
74 let account = config.account;
75 let options = config.options;
76 let username = account.username.clone();
77
78 let rt = tokio::runtime::Builder::new_current_thread()
81 .build()
82 .context("build worker tokio runtime")?;
83 let session = BaresipBackend
84 .spawn_session(rt.handle(), &config.name, &account, &options)
85 .with_context(|| format!("spawn backend for `{}`", config.name))?;
86
87 let events = session.events;
88 let phone = session.phone;
89 let header_poll = session.header_poll;
90 let handle = session.handle;
91
92 let (tx, rx) = std::sync::mpsc::sync_channel::<Out>(WRITER_CHANNEL_BOUND);
98 let writer = std::thread::spawn(move || {
99 let stdout = std::io::stdout();
100 while let Ok(msg) = rx.recv() {
101 let mut out = stdout.lock();
102 let res = match msg {
103 Out::Control(c) => match serde_json::to_vec(&c) {
104 Ok(bytes) => proto::write_frame(&mut out, proto::FRAME_CONTROL, &bytes),
105 Err(e) => {
106 ringo_core::rlog!(Warn, "serialize outbound: {e}");
107 continue;
108 }
109 },
110 Out::Audio(samples) => {
111 proto::write_frame(&mut out, proto::FRAME_AUDIO, &proto::pcm_to_bytes(&samples))
112 }
113 };
114 if res.is_err() {
115 break;
116 }
117 }
118 });
119
120 let _ = tx.send(Out::Control(FromWorker::Ready(Ready { ready: true })));
123
124 let ev_tx = tx.clone();
127 let event_bridge = std::thread::spawn(move || {
128 while let Ok(event) = events.recv() {
129 if ev_tx
130 .send(Out::Control(FromWorker::Event(WireEvent::from(&event))))
131 .is_err()
132 {
133 break;
134 }
135 }
136 });
137
138 let stop = Arc::new(AtomicBool::new(false));
140 let header_thread = header_poll.map(|poll| {
141 let hdr_tx = tx.clone();
142 let stop = Arc::clone(&stop);
143 std::thread::spawn(move || {
144 while !stop.load(Ordering::Relaxed) {
145 std::thread::sleep(HEADER_POLL_INTERVAL);
146 if let Some(headers) = poll() {
151 if !headers.is_empty()
152 && hdr_tx
153 .send(Out::Control(FromWorker::Headers(Headers { headers })))
154 .is_err()
155 {
156 break;
157 }
158 }
159 }
160 })
161 });
162
163 let rx_stop = Arc::new(AtomicBool::new(false));
165 let mut rx_forwarder: Option<JoinHandle<()>> = None;
166
167 loop {
169 let frame = match proto::read_frame(&mut stdin) {
170 Ok(Some(f)) => f,
171 Ok(None) => break, Err(e) => {
173 ringo_core::rlog!(Warn, "worker stdin read error: {e}");
174 break;
175 }
176 };
177 match frame {
178 (proto::FRAME_AUDIO, payload) => {
180 ringo_core::push_audio(&username, &proto::bytes_to_pcm(&payload));
181 }
182 (proto::FRAME_CONTROL, payload) => match serde_json::from_slice::<ToWorker>(&payload) {
183 Ok(ToWorker::Cmd(Command::Shutdown)) => break,
184 Ok(ToWorker::Cmd(Command::StartTxAudio { rate })) => {
185 ringo_core::start_audio_stream(&username, rate);
186 }
187 Ok(ToWorker::Cmd(Command::StartRxAudio)) => {
188 if rx_forwarder.is_none() {
189 rx_forwarder = Some(spawn_rx_forwarder(
190 &username,
191 tx.clone(),
192 Arc::clone(&rx_stop),
193 ));
194 }
195 }
196 Ok(ToWorker::Cmd(cmd)) => dispatch(phone.as_ref(), cmd),
197 Ok(ToWorker::Query(QueryEnvelope { id, query })) => {
198 let result = answer(phone.as_ref(), &username, query);
199 if tx
200 .send(Out::Control(FromWorker::Reply(Reply { reply: id, result })))
201 .is_err()
202 {
203 break;
204 }
205 }
206 Err(e) => ringo_core::rlog!(
209 Warn,
210 "ignoring malformed control frame ({} bytes): {e}",
211 payload.len()
212 ),
213 },
214 (_kind, _) => {} }
216 }
217
218 stop.store(true, Ordering::Relaxed);
221 rx_stop.store(true, Ordering::Relaxed);
222 if let Some(h) = header_thread {
223 let _ = h.join();
224 }
225 if let Some(h) = rx_forwarder {
226 let _ = h.join();
227 }
228 drop(phone);
229 drop(handle); ringo_core::shutdown();
234 let _ = event_bridge.join();
235 drop(tx);
236 let _ = writer.join();
237 Ok(())
238}
239
240fn spawn_rx_forwarder(
247 username: &str,
248 tx: SyncSender<Out>,
249 stop: Arc<AtomicBool>,
250) -> JoinHandle<()> {
251 let rx = ringo_core::subscribe_received_audio(username);
252 std::thread::spawn(move || {
253 let mut announced_rate: Option<u32> = None;
254 while !stop.load(Ordering::Relaxed) {
255 match rx.recv_timeout(Duration::from_millis(200)) {
256 Ok(frame) => {
257 if announced_rate != Some(frame.rate) {
258 announced_rate = Some(frame.rate);
259 let started = FromWorker::RxAudioStarted(RxAudioStarted {
260 rx_audio_rate: frame.rate,
261 });
262 if tx.send(Out::Control(started)).is_err() {
263 break;
264 }
265 }
266 if tx.send(Out::Audio(frame.samples)).is_err() {
267 break;
268 }
269 }
270 Err(RecvTimeoutError::Timeout) => continue, Err(RecvTimeoutError::Disconnected) => break,
272 }
273 }
274 })
275}
276
277fn init_logging(name: &str) {
282 if let Some(t) = std::env::var_os("RINGO_AGENT_LOG").and_then(|v| v.into_string().ok()) {
283 match t.as_str() {
284 "" => {}
285 "-" => ringo_core::log::init_stderr(),
286 path => ringo_core::log::init_file(per_agent_path(path, name)),
287 }
288 }
289 if let Some(t) = std::env::var_os("RINGO_AGENT_SIPTRACE").and_then(|v| v.into_string().ok()) {
290 match t.as_str() {
291 "" => {}
292 "-" => ringo_core::sip_trace_stderr(),
293 path => ringo_core::sip_trace_file(per_agent_path(path, name)),
294 }
295 }
296}
297
298fn per_agent_path(base: &str, name: &str) -> std::path::PathBuf {
303 let safe: String = name
304 .chars()
305 .map(|c| {
306 if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
307 c
308 } else {
309 '_'
310 }
311 })
312 .collect();
313 let tag = format!("{safe}.{}", std::process::id());
314 let p = std::path::Path::new(base);
315 match p.extension().and_then(|e| e.to_str()) {
316 Some(ext) => p.with_extension(format!("{tag}.{ext}")),
317 None => p.with_extension(tag),
318 }
319}
320
321fn dispatch(phone: &dyn Phone, cmd: Command) {
323 match cmd {
324 Command::Register { aor, regint } => phone.register(&aor, regint),
325 Command::Dial { number } => phone.dial(&number),
326 Command::Hangup => phone.hangup(),
327 Command::HangupAll => phone.hangup_all(),
328 Command::Accept => phone.accept(),
329 Command::Hold => phone.hold(),
330 Command::Resume => phone.resume(),
331 Command::Mute => phone.mute(),
332 Command::Dtmf { digit } => phone.send_dtmf(digit),
333 Command::SwitchLine { line } => phone.switch_line(line),
334 Command::Transfer { uri } => phone.transfer(&uri),
335 Command::AttendedTransferStart { uri } => phone.attended_transfer_start(&uri),
336 Command::AttendedTransferExec => phone.attended_transfer_exec(),
337 Command::AttendedTransferAbort => phone.attended_transfer_abort(),
338 Command::AddHeader { key, value } => phone.add_header(&key, &value),
339 Command::RmHeader { key } => phone.rm_header(&key),
340 Command::SetAudioSource { spec } => phone.set_audio_source(&spec),
341 Command::ArmInviteResponse {
342 scode,
343 reason,
344 headers,
345 } => phone.arm_invite_response(scode, &reason, headers),
346 Command::DisarmInviteResponse => phone.disarm_invite_response(),
347 Command::StartTxAudio { .. } | Command::StartRxAudio | Command::Shutdown => {}
350 }
351}
352
353fn answer(phone: &dyn Phone, username: &str, query: Query) -> ReplyResult {
357 match query {
358 Query::MediaStats => ReplyResult::MediaStats(phone.media_stats().map(WireMediaStats::from)),
359 Query::ReceivedDtmf => ReplyResult::Dtmf(phone.received_dtmf()),
360 Query::SaveAudio { prefix } => {
361 let mut written = Vec::new();
362 for (tag, captured) in [
363 ("sent", ringo_core::sent_audio(username)),
364 ("recv", ringo_core::received_audio(username)),
365 ] {
366 let Some((samples, srate)) = captured else {
367 continue;
368 };
369 if samples.is_empty() {
370 continue;
371 }
372 let path = format!("{prefix}-{tag}.wav");
373 match audio::write_wav(std::path::Path::new(&path), &samples, srate) {
374 Ok(()) => written.push(path),
375 Err(e) => ringo_core::rlog!(Warn, "save {path}: {e}"),
376 }
377 }
378 ReplyResult::Saved(written)
379 }
380 Query::CallCount => ReplyResult::CallCount(ringo_core::call_count() as u32),
381 }
382}