mirror of
https://github.com/openai/codex.git
synced 2026-05-05 03:47:01 +00:00
Redirect debug client output to a file (#17234)
In the app-server debug client, allow redirecting output to a file in addition to just stdout. Shell redirecting works OK but is a bit weird with the interactive mode of the debug client since a bunch of newlines get dumped into the shell. With async messages from MCPs starting it's also tricky to actually type in a prompt.
This commit is contained in:
@@ -299,8 +299,8 @@ impl AppServerClient {
|
||||
}
|
||||
|
||||
let line = buffer.trim_end_matches(['\n', '\r']);
|
||||
if !line.is_empty() && !self.filtered_output {
|
||||
let _ = output.server_line(line);
|
||||
if !line.is_empty() {
|
||||
let _ = output.server_json_line(line, self.filtered_output);
|
||||
}
|
||||
|
||||
let message = match serde_json::from_str::<JSONRPCMessage>(line) {
|
||||
|
||||
@@ -4,8 +4,10 @@ mod output;
|
||||
mod reader;
|
||||
mod state;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -50,6 +52,10 @@ struct Cli {
|
||||
#[arg(long, default_value_t = false)]
|
||||
final_only: bool,
|
||||
|
||||
/// Write raw server JSONL to this file instead of stdout.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
output_file: Option<PathBuf>,
|
||||
|
||||
/// Optional model override when starting/resuming a thread.
|
||||
#[arg(long)]
|
||||
model: Option<String>,
|
||||
@@ -65,7 +71,18 @@ struct Cli {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let output = Output::new();
|
||||
let jsonl_file = cli
|
||||
.output_file
|
||||
.as_ref()
|
||||
.map(File::create)
|
||||
.transpose()
|
||||
.with_context(|| {
|
||||
let Some(path) = cli.output_file.as_ref() else {
|
||||
return "open output file".to_string();
|
||||
};
|
||||
format!("open output file {}", path.display())
|
||||
})?;
|
||||
let output = Output::new(jsonl_file);
|
||||
let approval_policy = parse_approval_policy(&cli.approval_policy)?;
|
||||
|
||||
let mut client = AppServerClient::spawn(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#![allow(clippy::expect_used)]
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::IsTerminal;
|
||||
use std::io::Write;
|
||||
@@ -24,19 +25,41 @@ pub struct Output {
|
||||
lock: Arc<Mutex<()>>,
|
||||
prompt: Arc<Mutex<PromptState>>,
|
||||
color: bool,
|
||||
jsonl_file: Option<Arc<Mutex<File>>>,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(jsonl_file: Option<File>) -> Self {
|
||||
let no_color = std::env::var_os("NO_COLOR").is_some();
|
||||
let color = !no_color && io::stdout().is_terminal() && io::stderr().is_terminal();
|
||||
Self {
|
||||
lock: Arc::new(Mutex::new(())),
|
||||
prompt: Arc::new(Mutex::new(PromptState::default())),
|
||||
color,
|
||||
jsonl_file: jsonl_file.map(|file| Arc::new(Mutex::new(file))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_json_line(&self, line: &str, filtered_output: bool) -> io::Result<()> {
|
||||
let _guard = self.lock.lock().expect("output lock poisoned");
|
||||
|
||||
if let Some(file) = self.jsonl_file.as_ref() {
|
||||
let mut file = file.lock().expect("jsonl file lock poisoned");
|
||||
writeln!(file, "{line}")?;
|
||||
file.flush()?;
|
||||
}
|
||||
|
||||
if self.jsonl_file.is_none() && !filtered_output {
|
||||
self.clear_prompt_line_locked()?;
|
||||
let mut stdout = io::stdout();
|
||||
writeln!(stdout, "{line}")?;
|
||||
stdout.flush()?;
|
||||
self.redraw_prompt_locked()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn server_line(&self, line: &str) -> io::Result<()> {
|
||||
let _guard = self.lock.lock().expect("output lock poisoned");
|
||||
self.clear_prompt_line_locked()?;
|
||||
@@ -120,3 +143,33 @@ impl Output {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn server_json_line_writes_to_configured_file() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"codex-debug-client-output-{}.jsonl",
|
||||
std::process::id()
|
||||
));
|
||||
let file = File::create(&path).expect("create output file");
|
||||
let output = Output::new(Some(file));
|
||||
|
||||
output
|
||||
.server_json_line(r#"{"id":1}"#, false)
|
||||
.expect("write unfiltered line");
|
||||
output
|
||||
.server_json_line(r#"{"id":2}"#, true)
|
||||
.expect("write filtered line");
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(&path).expect("read output file"),
|
||||
"{\"id\":1}\n{\"id\":2}\n"
|
||||
);
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ pub fn start_reader(
|
||||
}
|
||||
|
||||
let line = buffer.trim_end_matches(['\n', '\r']);
|
||||
if !line.is_empty() && !filtered_output {
|
||||
let _ = output.server_line(line);
|
||||
if !line.is_empty() {
|
||||
let _ = output.server_json_line(line, filtered_output);
|
||||
}
|
||||
|
||||
let Ok(message) = serde_json::from_str::<JSONRPCMessage>(line) else {
|
||||
|
||||
Reference in New Issue
Block a user