mirror of
https://github.com/openai/codex.git
synced 2026-09-11 20:36:49 +00:00
## What changed - Add `RealtimeWebrtcSession` and cloneable handles for startup, answer negotiation, audio controls, level meters, and error reporting. - Open devices after answer negotiation and apply the latest controls before enabling audio. Preserve subsequent control transitions in order and close the session if the command queue fills. - Cancel pending work on explicit close, external cancellation, or final handle drop, with a shared runtime keeping helper reaping alive. - Add audio-state polling to consume microphone and speaker peaks and detect helper, device, or connection failures. - Check packaged helper/runtime availability and initialize build information in the standalone TUI for the helper handshake. ## Testing Add unit and helper-process integration tests covering startup control ordering, queue overflow, cancellation, helper reaping and loss, package availability, SDP debug redaction, and peak accumulation, clamping, and consumption. GitOrigin-RevId: 458cd80353697f7411cee785f380d1d53aef8416
220 lines
7.5 KiB
Rust
220 lines
7.5 KiB
Rust
//! Copies this test executable into a private package and dispatches its fake helper before libtest.
|
|
//! The fixture validates protocol order; it never loads native libraries or opens audio devices.
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use anyhow::ensure;
|
|
use codex_realtime_webrtc::AudioState;
|
|
use codex_realtime_webrtc::Message;
|
|
use codex_realtime_webrtc::encode_frame;
|
|
use codex_realtime_webrtc::read_message;
|
|
use std::fs;
|
|
use std::io::Write;
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
use std::process::Stdio;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
use std::time::SystemTime;
|
|
use std::time::UNIX_EPOCH;
|
|
|
|
pub const BUILD_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567";
|
|
pub const WAIT: Duration = Duration::from_secs(/*secs*/ 10);
|
|
const APP: &str = if cfg!(windows) { "codex.exe" } else { "codex" };
|
|
const HELPER: &str = if cfg!(windows) {
|
|
"codex-voice-host.exe"
|
|
} else {
|
|
"codex-voice-host"
|
|
};
|
|
|
|
pub fn wait_for(mut ready: impl FnMut() -> bool) -> Result<()> {
|
|
let deadline = Instant::now() + WAIT;
|
|
while !ready() {
|
|
ensure!(Instant::now() < deadline, "fixture deadline expired");
|
|
thread::sleep(Duration::from_millis(/*millis*/ 5));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
pub fn wait_for_helper_reaped(root: &std::path::Path) -> Result<()> {
|
|
let pid: libc::pid_t = fs::read_to_string(root.join("helper-pid"))?.parse()?;
|
|
wait_for(|| {
|
|
// SAFETY: Signal zero checks process existence without delivering a signal.
|
|
let result = unsafe {
|
|
libc::kill(pid, /*sig*/ 0)
|
|
};
|
|
result == -1 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
|
|
})
|
|
}
|
|
|
|
struct Package(PathBuf);
|
|
impl Drop for Package {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
// The child discovers its installation through current_exe, exactly as the public facade does.
|
|
pub fn package(test: &str) -> Result<Option<PathBuf>> {
|
|
let source = std::env::current_exe()?;
|
|
if source.file_name().is_some_and(|name| name == APP) {
|
|
codex_build_info::BuildInfo::initialize(BUILD_COMMIT);
|
|
return Ok(Some(
|
|
source
|
|
.parent()
|
|
.context("bin")?
|
|
.parent()
|
|
.context("package")?
|
|
.to_owned(),
|
|
));
|
|
}
|
|
let nonce = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
|
|
let package =
|
|
Package(std::env::temp_dir().join(format!("voice-actor-{}-{nonce}", std::process::id())));
|
|
let root = &package.0;
|
|
fs::create_dir_all(root.join("bin"))?;
|
|
fs::create_dir_all(root.join("codex-resources/voice/bin"))?;
|
|
fs::write(root.join("codex-package.json"), "{}")?;
|
|
fs::copy(&source, root.join("bin").join(APP))?;
|
|
fs::copy(&source, root.join("codex-resources/voice/bin").join(HELPER))?;
|
|
let mut child = Command::new(root.join("bin").join(APP))
|
|
.args(["--exact", test, "--nocapture"])
|
|
.current_dir(root)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()?;
|
|
let deadline = Instant::now() + WAIT * 3;
|
|
while child.try_wait()?.is_none() {
|
|
if Instant::now() >= deadline {
|
|
child.kill()?;
|
|
child.wait()?;
|
|
anyhow::bail!("packaged test timed out");
|
|
}
|
|
thread::sleep(Duration::from_millis(/*millis*/ 10));
|
|
}
|
|
let output = child.wait_with_output()?;
|
|
ensure!(
|
|
output.status.success(),
|
|
"packaged test failed: {}\n{}\nfixture: {}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr),
|
|
fs::read_to_string(root.join("fixture-error")).unwrap_or_default()
|
|
);
|
|
Ok(None)
|
|
}
|
|
|
|
#[ctor::ctor]
|
|
fn dispatch_helper() {
|
|
if std::env::current_exe()
|
|
.ok()
|
|
.and_then(|path| path.file_name().map(std::ffi::OsStr::to_owned))
|
|
.is_some_and(|name| name == HELPER)
|
|
{
|
|
if let Err(error) = helper() {
|
|
let _ = fs::write("fixture-error", format!("{error:#}"));
|
|
std::process::exit(1);
|
|
}
|
|
std::process::exit(0);
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
enum Stage {
|
|
Hello,
|
|
Initialize,
|
|
Offer,
|
|
Answer,
|
|
Devices,
|
|
Controls,
|
|
Running,
|
|
}
|
|
|
|
fn helper() -> Result<()> {
|
|
let root = std::env::current_dir()?;
|
|
fs::write(root.join("helper-pid"), std::process::id().to_string())?;
|
|
let mut input = std::io::stdin().lock();
|
|
let mut output = std::io::stdout().lock();
|
|
let mut stage = Stage::Hello;
|
|
let mut controls_history = Vec::new();
|
|
while let Some(request) = read_message(&mut input)? {
|
|
let response = match request {
|
|
Message::Hello {
|
|
protocol: 1,
|
|
build_commit,
|
|
} if stage == Stage::Hello => {
|
|
ensure!(build_commit == BUILD_COMMIT, "wrong executable build stamp");
|
|
stage = Stage::Initialize;
|
|
Message::Ready {}
|
|
}
|
|
Message::InitializeRuntime {} if stage == Stage::Initialize => {
|
|
fs::write(root.join("initializing"), [])?;
|
|
if root.join("hold-initialization").exists() {
|
|
wait_for(|| root.join("release").exists())?;
|
|
}
|
|
stage = Stage::Offer;
|
|
Message::RuntimeReady {}
|
|
}
|
|
Message::StartTransport {} if stage == Stage::Offer => {
|
|
stage = Stage::Answer;
|
|
Message::Offer {
|
|
sdp: "synthetic-offer"
|
|
.to_owned()
|
|
.try_into()
|
|
.map_err(anyhow::Error::msg)?,
|
|
}
|
|
}
|
|
Message::ApplyAnswer { sdp } if stage == Stage::Answer => {
|
|
ensure!(
|
|
sdp.into_sdp() == "synthetic-answer",
|
|
"unexpected fixture answer"
|
|
);
|
|
fs::write(root.join("answer"), [])?;
|
|
wait_for(|| root.join("release").exists())?;
|
|
stage = Stage::Devices;
|
|
Message::TransportReady {}
|
|
}
|
|
Message::OpenDevices {} if stage == Stage::Devices => {
|
|
stage = Stage::Controls;
|
|
Message::DevicesOpened {}
|
|
}
|
|
Message::SetAudioControls { controls }
|
|
if stage == Stage::Controls || stage == Stage::Running =>
|
|
{
|
|
controls_history.push(controls);
|
|
fs::write(
|
|
root.join("controls"),
|
|
serde_json::to_vec(&controls_history)?,
|
|
)?;
|
|
stage = Stage::Running;
|
|
Message::AudioControlsApplied {}
|
|
}
|
|
Message::InspectAudio {} if stage == Stage::Answer || stage == Stage::Running => {
|
|
if root.join("exit").exists() {
|
|
return Ok(());
|
|
}
|
|
Message::AudioState {
|
|
state: if stage == Stage::Running {
|
|
AudioState {
|
|
microphone_peak: 123,
|
|
speaker_peak: 456,
|
|
}
|
|
} else {
|
|
AudioState::default()
|
|
},
|
|
}
|
|
}
|
|
Message::Close {} => {
|
|
output.write_all(&encode_frame(&Message::Closed {})?)?;
|
|
output.flush()?;
|
|
return Ok(());
|
|
}
|
|
_ => anyhow::bail!("unexpected fixture protocol sequence"),
|
|
};
|
|
output.write_all(&encode_frame(&response)?)?;
|
|
output.flush()?;
|
|
}
|
|
Ok(())
|
|
}
|