1use crate::audio;
15use crate::proto::{
16 self, Command, FromWorker, Headers, Query, QueryEnvelope, Ready, Reply, ReplyResult,
17 RxAudioStarted, ToWorker, WireEvent, WireMediaStats, WireSavedAudio,
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(Command::SetRecording { on })) => {
197 ringo_core::set_recording(&username, on);
198 }
199 Ok(ToWorker::Cmd(cmd)) => dispatch(phone.as_ref(), cmd),
200 Ok(ToWorker::Query(QueryEnvelope { id, query })) => {
201 let result = answer(phone.as_ref(), &username, query);
202 if tx
203 .send(Out::Control(FromWorker::Reply(Reply { reply: id, result })))
204 .is_err()
205 {
206 break;
207 }
208 }
209 Err(e) => ringo_core::rlog!(
212 Warn,
213 "ignoring malformed control frame ({} bytes): {e}",
214 payload.len()
215 ),
216 },
217 (_kind, _) => {} }
219 }
220
221 stop.store(true, Ordering::Relaxed);
224 rx_stop.store(true, Ordering::Relaxed);
225 if let Some(h) = header_thread {
226 let _ = h.join();
227 }
228 if let Some(h) = rx_forwarder {
229 let _ = h.join();
230 }
231 drop(phone);
232 drop(handle); ringo_core::shutdown();
237 let _ = event_bridge.join();
238 drop(tx);
239 let _ = writer.join();
240 Ok(())
241}
242
243fn spawn_rx_forwarder(
250 username: &str,
251 tx: SyncSender<Out>,
252 stop: Arc<AtomicBool>,
253) -> JoinHandle<()> {
254 let rx = ringo_core::subscribe_received_audio(username);
255 std::thread::spawn(move || {
256 let mut announced_rate: Option<u32> = None;
257 while !stop.load(Ordering::Relaxed) {
258 match rx.recv_timeout(Duration::from_millis(200)) {
259 Ok(frame) => {
260 if announced_rate != Some(frame.rate) {
261 announced_rate = Some(frame.rate);
262 let started = FromWorker::RxAudioStarted(RxAudioStarted {
263 rx_audio_rate: frame.rate,
264 });
265 if tx.send(Out::Control(started)).is_err() {
266 break;
267 }
268 }
269 if tx.send(Out::Audio(frame.samples)).is_err() {
270 break;
271 }
272 }
273 Err(RecvTimeoutError::Timeout) => continue, Err(RecvTimeoutError::Disconnected) => break,
275 }
276 }
277 })
278}
279
280fn init_logging(name: &str) {
285 if let Some(t) = std::env::var_os("RINGO_AGENT_LOG").and_then(|v| v.into_string().ok()) {
286 match t.as_str() {
287 "" => {}
288 "-" => ringo_core::log::init_stderr(),
289 path => ringo_core::log::init_file(per_agent_path(path, name, std::process::id())),
290 }
291 }
292 if let Some(t) = std::env::var_os("RINGO_AGENT_SIPTRACE").and_then(|v| v.into_string().ok()) {
293 match t.as_str() {
294 "" => {}
295 "-" => ringo_core::sip_trace_stderr(),
296 path => ringo_core::sip_trace_file(per_agent_path(path, name, std::process::id())),
297 }
298 }
299}
300
301pub fn per_agent_path(base: &str, name: &str, pid: u32) -> std::path::PathBuf {
307 let safe: String = name
308 .chars()
309 .map(|c| {
310 if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
311 c
312 } else {
313 '_'
314 }
315 })
316 .collect();
317 let tag = format!("{safe}.{pid}");
318 let p = std::path::Path::new(base);
319 match p.extension().and_then(|e| e.to_str()) {
320 Some(ext) => p.with_extension(format!("{tag}.{ext}")),
321 None => p.with_extension(tag),
322 }
323}
324
325fn dispatch(phone: &dyn Phone, cmd: Command) {
327 match cmd {
328 Command::Register { aor, regint } => phone.register(&aor, regint),
329 Command::Dial { number } => phone.dial(&number),
330 Command::Hangup => phone.hangup(),
331 Command::HangupAll => phone.hangup_all(),
332 Command::Accept => phone.accept(),
333 Command::Hold => phone.hold(),
334 Command::Resume => phone.resume(),
335 Command::Mute => phone.mute(),
336 Command::Dtmf { digit } => phone.send_dtmf(digit),
337 Command::SwitchLine { line } => phone.switch_line(line),
338 Command::Transfer { uri } => phone.transfer(&uri),
339 Command::AttendedTransferStart { uri } => phone.attended_transfer_start(&uri),
340 Command::AttendedTransferExec => phone.attended_transfer_exec(),
341 Command::AttendedTransferAbort => phone.attended_transfer_abort(),
342 Command::AddHeader { key, value } => phone.add_header(&key, &value),
343 Command::RmHeader { key } => phone.rm_header(&key),
344 Command::SetAudioSource { spec } => phone.set_audio_source(&spec),
345 Command::ArmInviteResponse {
346 scode,
347 reason,
348 headers,
349 } => phone.arm_invite_response(scode, &reason, headers),
350 Command::DisarmInviteResponse => phone.disarm_invite_response(),
351 Command::StartTxAudio { .. }
355 | Command::StartRxAudio
356 | Command::SetRecording { .. }
357 | Command::Shutdown => {}
358 }
359}
360
361fn answer(phone: &dyn Phone, username: &str, query: Query) -> ReplyResult {
365 match query {
366 Query::MediaStats => ReplyResult::MediaStats(phone.media_stats().map(WireMediaStats::from)),
367 Query::ReceivedDtmf => ReplyResult::Dtmf(phone.received_dtmf()),
368 Query::SaveAudio { prefix } => {
369 let mut written = Vec::new();
370 for (tag, captured) in [
371 ("sent", ringo_core::sent_audio(username)),
372 ("recv", ringo_core::received_audio(username)),
373 ] {
374 let Some((samples, srate)) = captured else {
375 continue;
376 };
377 if samples.is_empty() {
378 continue;
379 }
380 let truncated =
381 samples.len() >= srate as usize * ringo_core::RECORDING_MAX_SECS as usize;
382 let path = format!("{prefix}-{tag}.wav");
383 match audio::write_wav(std::path::Path::new(&path), &samples, srate) {
384 Ok(()) => written.push(WireSavedAudio {
385 path,
386 kind: tag.into(),
387 samples: samples.len(),
388 srate,
389 truncated,
390 }),
391 Err(e) => ringo_core::rlog!(Warn, "save {path}: {e}"),
392 }
393 }
394 ReplyResult::SavedAudio(written)
395 }
396 Query::CallCount => ReplyResult::CallCount(ringo_core::call_count() as u32),
397 }
398}