mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(logging): overhaul debug log handling
This commit is contained in:
@@ -13,6 +13,7 @@ use once_cell::sync::Lazy;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -85,6 +86,43 @@ fn is_temporary_internal_launch_error_message(message: &str) -> bool {
|
||||
|| message.contains("os error 24")
|
||||
}
|
||||
|
||||
fn chrome_logging_enabled() -> bool {
|
||||
env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("CODEX_BROWSER_LOG")
|
||||
}
|
||||
|
||||
fn env_truthy(key: &str) -> bool {
|
||||
std::env::var(key)
|
||||
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn resolve_chrome_log_path() -> Option<PathBuf> {
|
||||
if !chrome_logging_enabled() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Ok(path) = std::env::var("CODEX_BROWSER_LOG_PATH") {
|
||||
let trimmed = path.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Some(PathBuf::from(trimmed));
|
||||
}
|
||||
}
|
||||
|
||||
let base = if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("CODEX_HOME")) {
|
||||
PathBuf::from(home).join("debug_logs")
|
||||
} else if let Ok(home) = std::env::var("HOME") {
|
||||
PathBuf::from(home).join(".code").join("debug_logs")
|
||||
} else {
|
||||
return Some(std::env::temp_dir().join("code-chrome.log"));
|
||||
};
|
||||
|
||||
let path = base.join("code-chrome.log");
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
Some(path)
|
||||
}
|
||||
|
||||
const HANDLER_ERROR_LIMIT: u32 = 3;
|
||||
|
||||
fn should_restart_handler(consecutive_errors: u32) -> bool {
|
||||
@@ -771,7 +809,7 @@ impl BrowserManager {
|
||||
let max_attempts = INTERNAL_LAUNCH_RETRY_DELAYS_MS.len() + 1;
|
||||
|
||||
// Add browser launch flags (keep minimal set for screenshot functionality)
|
||||
let log_file = format!("{}/code-chrome.log", std::env::temp_dir().display());
|
||||
let log_file = resolve_chrome_log_path();
|
||||
|
||||
let (browser, mut handler, user_data_path) = {
|
||||
let mut attempt = 1usize;
|
||||
@@ -814,15 +852,18 @@ impl BrowserManager {
|
||||
// Disable timeout for slow networks/pages
|
||||
.arg("--disable-hang-monitor")
|
||||
.arg("--disable-background-timer-throttling")
|
||||
// Redirect Chrome logging to a file to keep terminal clean
|
||||
.arg("--enable-logging")
|
||||
.arg("--log-level=1") // 0 = INFO, 1 = WARNING, 2 = ERROR, 3 = FATAL (1 to reduce verbosity)
|
||||
.arg(format!("--log-file={log_file}"))
|
||||
// Suppress console output
|
||||
.arg("--silent-launch")
|
||||
// Set a longer timeout for CDP requests (60 seconds instead of default 30)
|
||||
.request_timeout(Duration::from_secs(60));
|
||||
|
||||
if let Some(ref log_file) = log_file {
|
||||
builder = builder
|
||||
.arg("--enable-logging")
|
||||
.arg("--log-level=1")
|
||||
.arg(format!("--log-file={}", log_file.display()));
|
||||
}
|
||||
|
||||
let browser_config = builder
|
||||
.build()
|
||||
.map_err(|e| BrowserError::CdpError(e.to_string()))?;
|
||||
@@ -859,8 +900,12 @@ impl BrowserManager {
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
let hint = "Ensure Chrome/Chromium is installed and available on PATH.";
|
||||
|
||||
let log_note = log_file
|
||||
.as_ref()
|
||||
.map(|path| format!(" Chrome log: {}", path.display()))
|
||||
.unwrap_or_default();
|
||||
return Err(BrowserError::CdpError(format!(
|
||||
"Failed to launch internal browser: {message}. Hint: {hint}. Chrome log: {log_file}"
|
||||
"Failed to launch internal browser: {message}. Hint: {hint}.{log_note}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,8 +378,7 @@ struct OrderReplayArgs {
|
||||
/// Path to a response.json captured under ~/.code/debug_logs/*_response.json
|
||||
/// (legacy ~/.codex/debug_logs/ is still read).
|
||||
response_json: std::path::PathBuf,
|
||||
/// Path to codex-tui.log (typically ~/.code/log/codex-tui.log; legacy
|
||||
/// ~/.codex/log/codex-tui.log is still read).
|
||||
/// Path to codex-tui.log (typically ~/.code/debug_logs/codex-tui.log).
|
||||
tui_log: std::path::PathBuf,
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ use crate::TurnAttempt;
|
||||
use crate::api::TaskText;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use code_backend_client as backend;
|
||||
use code_backend_client::CodeTaskDetailsResponseExt;
|
||||
@@ -141,7 +144,7 @@ mod api {
|
||||
.map(map_task_list_item_to_summary)
|
||||
.collect();
|
||||
|
||||
append_error_log(&format!(
|
||||
append_debug_log(&format!(
|
||||
"http.list_tasks: env={} items={}",
|
||||
env.unwrap_or("<all>"),
|
||||
tasks.len()
|
||||
@@ -260,7 +263,7 @@ mod api {
|
||||
|
||||
match self.backend.create_task(request_body).await {
|
||||
Ok(id) => {
|
||||
append_error_log(&format!(
|
||||
append_info_log(&format!(
|
||||
"new_task: created id={id} env={} prompt_chars={}",
|
||||
env_id,
|
||||
prompt.chars().count()
|
||||
@@ -756,14 +759,190 @@ mod api {
|
||||
}
|
||||
}
|
||||
|
||||
const CLOUD_TASKS_LOG_FILE: &str = "cloud-tasks.log";
|
||||
const CLOUD_TASKS_LOG_MAX_BYTES: u64 = 5 * 1024 * 1024;
|
||||
const CLOUD_TASKS_LOG_BACKUPS: usize = 2;
|
||||
const CLOUD_TASKS_LOG_MAX_MESSAGE_BYTES: usize = 64 * 1024;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum CloudLogLevel {
|
||||
Off = 0,
|
||||
Error = 1,
|
||||
Info = 2,
|
||||
Debug = 3,
|
||||
}
|
||||
|
||||
fn log_level_from_env() -> CloudLogLevel {
|
||||
if let Ok(raw) = std::env::var("CODEX_CLOUD_TASKS_LOG_LEVEL") {
|
||||
let value = raw.trim().to_ascii_lowercase();
|
||||
return match value.as_str() {
|
||||
"off" | "none" | "0" => CloudLogLevel::Off,
|
||||
"error" | "warn" | "1" => CloudLogLevel::Error,
|
||||
"info" | "2" => CloudLogLevel::Info,
|
||||
"debug" | "trace" | "3" => CloudLogLevel::Debug,
|
||||
_ => CloudLogLevel::Error,
|
||||
};
|
||||
}
|
||||
|
||||
if env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("CODEX_CLOUD_TASKS_DEBUG") {
|
||||
return CloudLogLevel::Debug;
|
||||
}
|
||||
|
||||
CloudLogLevel::Off
|
||||
}
|
||||
|
||||
fn env_truthy(key: &str) -> bool {
|
||||
std::env::var(key)
|
||||
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn should_log(level: CloudLogLevel) -> bool {
|
||||
let configured = log_level_from_env();
|
||||
configured != CloudLogLevel::Off && level <= configured
|
||||
}
|
||||
|
||||
fn user_home_dir() -> Option<PathBuf> {
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
return Some(PathBuf::from(home));
|
||||
}
|
||||
if let Ok(home) = std::env::var("USERPROFILE") {
|
||||
return Some(PathBuf::from(home));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn resolve_log_path() -> Option<PathBuf> {
|
||||
if let Ok(path) = std::env::var("CODEX_CLOUD_TASKS_LOG_PATH") {
|
||||
let trimmed = path.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Some(PathBuf::from(trimmed));
|
||||
}
|
||||
}
|
||||
|
||||
let base = if let Ok(dir) = std::env::var("CODEX_CLOUD_TASKS_LOG_DIR") {
|
||||
PathBuf::from(dir)
|
||||
} else if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("CODEX_HOME")) {
|
||||
PathBuf::from(home).join("debug_logs")
|
||||
} else if let Some(home) = user_home_dir() {
|
||||
home.join(".code").join("debug_logs")
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(base.join(CLOUD_TASKS_LOG_FILE))
|
||||
}
|
||||
|
||||
fn ensure_parent_dir(path: &Path) {
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
}
|
||||
|
||||
fn rotate_log_file(path: &Path, max_bytes: u64, backups: usize) {
|
||||
if backups == 0 {
|
||||
let _ = std::fs::remove_file(path);
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(meta) = std::fs::metadata(path) else {
|
||||
return;
|
||||
};
|
||||
if meta.len() <= max_bytes {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
|
||||
return;
|
||||
};
|
||||
let Some(dir) = path.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let lock_path = dir.join(format!("{file_name}.rotate.lock"));
|
||||
let lock_file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&lock_path);
|
||||
let Ok(_lock_file) = lock_file else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(meta) = std::fs::metadata(path) else {
|
||||
let _ = std::fs::remove_file(&lock_path);
|
||||
return;
|
||||
};
|
||||
if meta.len() <= max_bytes {
|
||||
let _ = std::fs::remove_file(&lock_path);
|
||||
return;
|
||||
}
|
||||
|
||||
let oldest = dir.join(format!("{file_name}.{backups}"));
|
||||
let _ = std::fs::remove_file(&oldest);
|
||||
|
||||
if backups > 1 {
|
||||
for idx in (1..backups).rev() {
|
||||
let from = dir.join(format!("{file_name}.{idx}"));
|
||||
let to = dir.join(format!("{file_name}.{}", idx + 1));
|
||||
let _ = std::fs::rename(&from, &to);
|
||||
}
|
||||
}
|
||||
|
||||
let rotated = dir.join(format!("{file_name}.1"));
|
||||
let _ = std::fs::copy(path, &rotated);
|
||||
if let Ok(file) = std::fs::OpenOptions::new().write(true).open(path) {
|
||||
let _ = file.set_len(0);
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(&lock_path);
|
||||
}
|
||||
|
||||
fn truncate_message(message: &str, max_bytes: usize) -> Cow<'_, str> {
|
||||
if message.len() <= max_bytes {
|
||||
return Cow::Borrowed(message);
|
||||
}
|
||||
let bytes = message.as_bytes();
|
||||
let head = String::from_utf8_lossy(&bytes[..max_bytes]).to_string();
|
||||
Cow::Owned(format!("{head}\n...truncated..."))
|
||||
}
|
||||
|
||||
fn append_error_log(message: &str) {
|
||||
append_cloud_log(CloudLogLevel::Error, message);
|
||||
}
|
||||
|
||||
fn append_info_log(message: &str) {
|
||||
append_cloud_log(CloudLogLevel::Info, message);
|
||||
}
|
||||
|
||||
fn append_debug_log(message: &str) {
|
||||
append_cloud_log(CloudLogLevel::Debug, message);
|
||||
}
|
||||
|
||||
fn append_cloud_log(level: CloudLogLevel, message: &str) {
|
||||
if !should_log(level) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(path) = resolve_log_path() else {
|
||||
return;
|
||||
};
|
||||
ensure_parent_dir(&path);
|
||||
rotate_log_file(&path, CLOUD_TASKS_LOG_MAX_BYTES, CLOUD_TASKS_LOG_BACKUPS);
|
||||
|
||||
let ts = Utc::now().to_rfc3339();
|
||||
let level_label = match level {
|
||||
CloudLogLevel::Error => "ERROR",
|
||||
CloudLogLevel::Info => "INFO",
|
||||
CloudLogLevel::Debug => "DEBUG",
|
||||
CloudLogLevel::Off => return,
|
||||
};
|
||||
let message = truncate_message(message, CLOUD_TASKS_LOG_MAX_MESSAGE_BYTES);
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("error.log")
|
||||
.open(&path)
|
||||
{
|
||||
use std::io::Write as _;
|
||||
let _ = writeln!(f, "[{ts}] {message}");
|
||||
let _ = writeln!(f, "[{ts}] {level_label} {message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub async fn autodetect_environment_id(
|
||||
) -> anyhow::Result<AutodetectSelection> {
|
||||
// 1) Try repo-specific environments based on local git origins (GitHub only, like VSCode)
|
||||
let origins = get_git_origins();
|
||||
crate::append_error_log(format!("env: git origins: {origins:?}"));
|
||||
crate::append_debug_log(format!("env: git origins: {origins:?}"));
|
||||
let mut by_repo_envs: Vec<CodeEnvironment> = Vec::new();
|
||||
for origin in &origins {
|
||||
if let Some((owner, repo)) = parse_owner_repo(origin) {
|
||||
@@ -43,16 +43,16 @@ pub async fn autodetect_environment_id(
|
||||
base_url, "github", owner, repo
|
||||
)
|
||||
};
|
||||
crate::append_error_log(format!("env: GET {url}"));
|
||||
crate::append_debug_log(format!("env: GET {url}"));
|
||||
match get_json::<Vec<CodeEnvironment>>(&url, headers).await {
|
||||
Ok(mut list) => {
|
||||
crate::append_error_log(format!(
|
||||
crate::append_debug_log(format!(
|
||||
"env: by-repo returned {} env(s) for {owner}/{repo}",
|
||||
list.len(),
|
||||
));
|
||||
by_repo_envs.append(&mut list);
|
||||
}
|
||||
Err(e) => crate::append_error_log(format!(
|
||||
Err(e) => crate::append_debug_log(format!(
|
||||
"env: by-repo fetch failed for {owner}/{repo}: {e}"
|
||||
)),
|
||||
}
|
||||
@@ -71,7 +71,7 @@ pub async fn autodetect_environment_id(
|
||||
} else {
|
||||
format!("{base_url}/api/codex/environments")
|
||||
};
|
||||
crate::append_error_log(format!("env: GET {list_url}"));
|
||||
crate::append_debug_log(format!("env: GET {list_url}"));
|
||||
// Fetch and log the full environments JSON for debugging
|
||||
let http = reqwest::Client::builder().build()?;
|
||||
let res = http.get(&list_url).headers(headers.clone()).send().await?;
|
||||
@@ -83,13 +83,13 @@ pub async fn autodetect_environment_id(
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
crate::append_error_log(format!("env: status={status} content-type={ct}"));
|
||||
crate::append_debug_log(format!("env: status={status} content-type={ct}"));
|
||||
match serde_json::from_str::<serde_json::Value>(&body) {
|
||||
Ok(v) => {
|
||||
let pretty = serde_json::to_string_pretty(&v).unwrap_or(body.clone());
|
||||
crate::append_error_log(format!("env: /environments JSON (pretty):\n{pretty}"));
|
||||
crate::append_debug_log(format!("env: /environments JSON (pretty):\n{pretty}"));
|
||||
}
|
||||
Err(_) => crate::append_error_log(format!("env: /environments (raw):\n{body}")),
|
||||
Err(_) => crate::append_debug_log(format!("env: /environments (raw):\n{body}")),
|
||||
}
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("GET {list_url} failed: {status}; content-type={ct}; body={body}");
|
||||
@@ -119,16 +119,16 @@ fn pick_environment_row(
|
||||
.iter()
|
||||
.find(|e| e.label.as_deref().unwrap_or("").to_lowercase() == lc)
|
||||
{
|
||||
crate::append_error_log(format!("env: matched by label: {label} -> {}", e.id));
|
||||
crate::append_debug_log(format!("env: matched by label: {label} -> {}", e.id));
|
||||
return Some(e.clone());
|
||||
}
|
||||
}
|
||||
if envs.len() == 1 {
|
||||
crate::append_error_log("env: single environment available; selecting it");
|
||||
crate::append_debug_log("env: single environment available; selecting it");
|
||||
return Some(envs[0].clone());
|
||||
}
|
||||
if let Some(e) = envs.iter().find(|e| e.is_pinned.unwrap_or(false)) {
|
||||
crate::append_error_log(format!("env: selecting pinned environment: {}", e.id));
|
||||
crate::append_debug_log(format!("env: selecting pinned environment: {}", e.id));
|
||||
return Some(e.clone());
|
||||
}
|
||||
// Highest task_count as heuristic
|
||||
@@ -137,7 +137,7 @@ fn pick_environment_row(
|
||||
.max_by_key(|e| e.task_count.unwrap_or(0))
|
||||
.or_else(|| envs.first())
|
||||
{
|
||||
crate::append_error_log(format!("env: selecting by task_count/first: {}", e.id));
|
||||
crate::append_debug_log(format!("env: selecting by task_count/first: {}", e.id));
|
||||
return Some(e.clone());
|
||||
}
|
||||
None
|
||||
@@ -157,7 +157,7 @@ async fn get_json<T: serde::de::DeserializeOwned>(
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
crate::append_error_log(format!("env: status={status} content-type={ct}"));
|
||||
crate::append_debug_log(format!("env: status={status} content-type={ct}"));
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("GET {url} failed: {status}; content-type={ct}; body={body}");
|
||||
}
|
||||
@@ -228,7 +228,7 @@ fn parse_owner_repo(url: &str) -> Option<(String, String)> {
|
||||
let mut parts = rest.splitn(2, '/');
|
||||
let owner = parts.next()?.to_string();
|
||||
let repo = parts.next()?.to_string();
|
||||
crate::append_error_log(format!("env: parsed SSH GitHub origin => {owner}/{repo}"));
|
||||
crate::append_debug_log(format!("env: parsed SSH GitHub origin => {owner}/{repo}"));
|
||||
return Some((owner, repo));
|
||||
}
|
||||
// HTTPS or git protocol
|
||||
@@ -243,7 +243,7 @@ fn parse_owner_repo(url: &str) -> Option<(String, String)> {
|
||||
let mut parts = rest.splitn(2, '/');
|
||||
let owner = parts.next()?.to_string();
|
||||
let repo = parts.next()?.to_string();
|
||||
crate::append_error_log(format!("env: parsed HTTP GitHub origin => {owner}/{repo}"));
|
||||
crate::append_debug_log(format!("env: parsed HTTP GitHub origin => {owner}/{repo}"));
|
||||
return Some((owner, repo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ use tokio::sync::mpsc::UnboundedSender;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use code_tui::public_widgets::composer_input::ComposerAction;
|
||||
use util::append_debug_log;
|
||||
use util::append_error_log;
|
||||
use util::append_info_log;
|
||||
use util::log_path_hint;
|
||||
use util::set_user_agent_suffix;
|
||||
|
||||
struct ApplyJob {
|
||||
@@ -192,7 +195,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
} else {
|
||||
"codex-api"
|
||||
};
|
||||
append_error_log(format!("startup: base_url={base_url} path_style={style}"));
|
||||
append_info_log(format!("startup: base_url={base_url} path_style={style}"));
|
||||
|
||||
// Require ChatGPT login (SWIC). Exit with a clear message if missing.
|
||||
let _token = match code_core::config::find_code_home()
|
||||
@@ -209,7 +212,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
Some(auth) => {
|
||||
// Log account context for debugging workspace selection.
|
||||
if let Some(acc) = auth.get_account_id() {
|
||||
append_error_log(format!("auth: mode=ChatGPT account_id={acc}"));
|
||||
append_info_log(format!("auth: mode=ChatGPT account_id={acc}"));
|
||||
}
|
||||
match auth.get_token().await {
|
||||
Ok(t) if !t.is_empty() => {
|
||||
@@ -219,7 +222,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
.get_account_id()
|
||||
.or_else(|| util::extract_chatgpt_account_id(&t))
|
||||
{
|
||||
append_error_log(format!("auth: set ChatGPT-Account-Id header: {acc}"));
|
||||
append_info_log(format!("auth: set ChatGPT-Account-Id header: {acc}"));
|
||||
http = http.with_chatgpt_account_id(acc);
|
||||
}
|
||||
t
|
||||
@@ -282,7 +285,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
.as_deref(),
|
||||
Some("1") | Some("true") | Some("TRUE")
|
||||
);
|
||||
append_error_log(format!(
|
||||
append_info_log(format!(
|
||||
"startup: wham_force_internal={} ua={}",
|
||||
force_internal,
|
||||
code_core::default_client::get_code_user_agent(None)
|
||||
@@ -432,7 +435,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
app::AppEvent::TasksLoaded { env, result } => {
|
||||
// Only apply results for the current filter to avoid races.
|
||||
if env.as_deref() != app.env_filter.as_deref() {
|
||||
append_error_log(format!(
|
||||
append_debug_log(format!(
|
||||
"refresh.drop: env={} current={}",
|
||||
env.clone().unwrap_or_else(|| "<all>".to_string()),
|
||||
app.env_filter.clone().unwrap_or_else(|| "<all>".to_string())
|
||||
@@ -442,7 +445,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
app.refresh_inflight = false;
|
||||
match result {
|
||||
Ok(tasks) => {
|
||||
append_error_log(format!(
|
||||
append_debug_log(format!(
|
||||
"refresh.apply: env={} count={}",
|
||||
env.clone().unwrap_or_else(|| "<all>".to_string()),
|
||||
tasks.len()
|
||||
@@ -462,7 +465,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
app::AppEvent::NewTaskSubmitted(result) => {
|
||||
match result {
|
||||
Ok(created) => {
|
||||
append_error_log(format!("new-task: created id={}", created.id.0));
|
||||
append_info_log(format!("new-task: created id={}", created.id.0));
|
||||
app.status = format!("Submitted as {}", created.id.0);
|
||||
app.new_task = None;
|
||||
// Refresh tasks in background for current filter
|
||||
@@ -482,7 +485,12 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
Err(msg) => {
|
||||
append_error_log(format!("new-task: submit failed: {msg}"));
|
||||
if let Some(page) = app.new_task.as_mut() { page.submitting = false; }
|
||||
app.status = format!("Submit failed: {msg}. See error.log for details.");
|
||||
if let Some(log_hint) = log_path_hint() {
|
||||
app.status =
|
||||
format!("Submit failed: {msg}. See {log_hint} for details.");
|
||||
} else {
|
||||
app.status = format!("Submit failed: {msg}.");
|
||||
}
|
||||
needs_redraw = true;
|
||||
let _ = frame_tx.send(Instant::now());
|
||||
}
|
||||
@@ -523,7 +531,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
if let Ok(sel) = result {
|
||||
// Only apply if user hasn't set a filter yet or it's different.
|
||||
if app.env_filter.as_deref() != Some(sel.id.as_str()) {
|
||||
append_error_log(format!(
|
||||
append_info_log(format!(
|
||||
"env.select: autodetected id={} label={}",
|
||||
sel.id,
|
||||
sel.label.clone().unwrap_or_else(|| "<none>".to_string())
|
||||
@@ -909,7 +917,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
if let Some(page) = app.new_task.as_mut() {
|
||||
page.best_of_n = new_value;
|
||||
}
|
||||
append_error_log(format!("best-of.select: attempts={new_value}"));
|
||||
append_info_log(format!("best-of.select: attempts={new_value}"));
|
||||
app.status = format!(
|
||||
"Best-of updated to {new_value} attempt{}",
|
||||
if new_value == 1 { "" } else { "s" }
|
||||
@@ -970,7 +978,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
} else if let ComposerAction::Submitted(text) = page.composer.input(key) {
|
||||
// Submit only if we have an env id
|
||||
if let Some(env) = page.env_id.clone() {
|
||||
append_error_log(format!(
|
||||
append_debug_log(format!(
|
||||
"new-task: submit env={} size={}",
|
||||
env,
|
||||
text.chars().count()
|
||||
@@ -1242,11 +1250,11 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
}).collect();
|
||||
// Keep original order (already sorted) — no need to re-sort
|
||||
let idx = state.selected;
|
||||
if idx == 0 { app.env_filter = None; append_error_log("env.select: All"); }
|
||||
if idx == 0 { app.env_filter = None; append_info_log("env.select: All"); }
|
||||
else {
|
||||
let env_idx = idx.saturating_sub(1);
|
||||
if let Some(row) = filtered.get(env_idx) {
|
||||
append_error_log(format!(
|
||||
append_info_log(format!(
|
||||
"env.select: id={} label={}",
|
||||
row.id,
|
||||
row.label.clone().unwrap_or_else(|| "<none>".to_string())
|
||||
@@ -1293,7 +1301,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
// Ensure 'r' does not refresh tasks when the env modal is open.
|
||||
KeyCode::Char('r') | KeyCode::Char('R') => {
|
||||
if app.env_modal.is_some() { break 0; }
|
||||
append_error_log(format!(
|
||||
append_debug_log(format!(
|
||||
"refresh.request: env={}",
|
||||
app.env_filter.clone().unwrap_or_else(|| "<all>".to_string())
|
||||
));
|
||||
|
||||
+206
-12
@@ -1,6 +1,212 @@
|
||||
use base64::Engine as _;
|
||||
use chrono::Utc;
|
||||
use reqwest::header::HeaderMap;
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const CLOUD_TASKS_LOG_FILE: &str = "cloud-tasks.log";
|
||||
const CLOUD_TASKS_LOG_MAX_BYTES: u64 = 5 * 1024 * 1024;
|
||||
const CLOUD_TASKS_LOG_BACKUPS: usize = 2;
|
||||
const CLOUD_TASKS_LOG_MAX_MESSAGE_BYTES: usize = 64 * 1024;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum CloudLogLevel {
|
||||
Off = 0,
|
||||
Error = 1,
|
||||
Info = 2,
|
||||
Debug = 3,
|
||||
}
|
||||
|
||||
fn log_level_from_env() -> CloudLogLevel {
|
||||
if let Ok(raw) = std::env::var("CODEX_CLOUD_TASKS_LOG_LEVEL") {
|
||||
let value = raw.trim().to_ascii_lowercase();
|
||||
return match value.as_str() {
|
||||
"off" | "none" | "0" => CloudLogLevel::Off,
|
||||
"error" | "warn" | "1" => CloudLogLevel::Error,
|
||||
"info" | "2" => CloudLogLevel::Info,
|
||||
"debug" | "trace" | "3" => CloudLogLevel::Debug,
|
||||
_ => CloudLogLevel::Error,
|
||||
};
|
||||
}
|
||||
|
||||
if env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("CODEX_CLOUD_TASKS_DEBUG") {
|
||||
return CloudLogLevel::Debug;
|
||||
}
|
||||
|
||||
CloudLogLevel::Off
|
||||
}
|
||||
|
||||
fn env_truthy(key: &str) -> bool {
|
||||
std::env::var(key)
|
||||
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn should_log(level: CloudLogLevel) -> bool {
|
||||
let configured = log_level_from_env();
|
||||
configured != CloudLogLevel::Off && level <= configured
|
||||
}
|
||||
|
||||
fn user_home_dir() -> Option<PathBuf> {
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
return Some(PathBuf::from(home));
|
||||
}
|
||||
if let Ok(home) = std::env::var("USERPROFILE") {
|
||||
return Some(PathBuf::from(home));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn resolve_log_path() -> Option<PathBuf> {
|
||||
if let Ok(path) = std::env::var("CODEX_CLOUD_TASKS_LOG_PATH") {
|
||||
let trimmed = path.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Some(PathBuf::from(trimmed));
|
||||
}
|
||||
}
|
||||
|
||||
let base = if let Ok(dir) = std::env::var("CODEX_CLOUD_TASKS_LOG_DIR") {
|
||||
PathBuf::from(dir)
|
||||
} else if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("CODEX_HOME")) {
|
||||
PathBuf::from(home).join("debug_logs")
|
||||
} else if let Some(home) = user_home_dir() {
|
||||
home.join(".code").join("debug_logs")
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(base.join(CLOUD_TASKS_LOG_FILE))
|
||||
}
|
||||
|
||||
fn ensure_parent_dir(path: &Path) {
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
}
|
||||
|
||||
fn rotate_log_file(path: &Path, max_bytes: u64, backups: usize) {
|
||||
if backups == 0 {
|
||||
let _ = std::fs::remove_file(path);
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(meta) = std::fs::metadata(path) else {
|
||||
return;
|
||||
};
|
||||
if meta.len() <= max_bytes {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
|
||||
return;
|
||||
};
|
||||
let Some(dir) = path.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let lock_path = dir.join(format!("{file_name}.rotate.lock"));
|
||||
let lock_file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&lock_path);
|
||||
let Ok(_lock_file) = lock_file else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(meta) = std::fs::metadata(path) else {
|
||||
let _ = std::fs::remove_file(&lock_path);
|
||||
return;
|
||||
};
|
||||
if meta.len() <= max_bytes {
|
||||
let _ = std::fs::remove_file(&lock_path);
|
||||
return;
|
||||
}
|
||||
|
||||
let oldest = dir.join(format!("{file_name}.{backups}"));
|
||||
let _ = std::fs::remove_file(&oldest);
|
||||
|
||||
if backups > 1 {
|
||||
for idx in (1..backups).rev() {
|
||||
let from = dir.join(format!("{file_name}.{idx}"));
|
||||
let to = dir.join(format!("{file_name}.{}", idx + 1));
|
||||
let _ = std::fs::rename(&from, &to);
|
||||
}
|
||||
}
|
||||
|
||||
let rotated = dir.join(format!("{file_name}.1"));
|
||||
let _ = std::fs::copy(path, &rotated);
|
||||
if let Ok(file) = std::fs::OpenOptions::new().write(true).open(path) {
|
||||
let _ = file.set_len(0);
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(&lock_path);
|
||||
}
|
||||
|
||||
fn truncate_message(message: &str, max_bytes: usize) -> Cow<'_, str> {
|
||||
if message.len() <= max_bytes {
|
||||
return Cow::Borrowed(message);
|
||||
}
|
||||
let bytes = message.as_bytes();
|
||||
let head = String::from_utf8_lossy(&bytes[..max_bytes]).to_string();
|
||||
Cow::Owned(format!("{head}\n...truncated..."))
|
||||
}
|
||||
|
||||
pub fn log_path_hint() -> Option<String> {
|
||||
if !logging_enabled() {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
resolve_log_path()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| CLOUD_TASKS_LOG_FILE.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn logging_enabled() -> bool {
|
||||
log_level_from_env() != CloudLogLevel::Off
|
||||
}
|
||||
|
||||
pub fn append_error_log(message: impl AsRef<str>) {
|
||||
append_cloud_log(CloudLogLevel::Error, message.as_ref());
|
||||
}
|
||||
|
||||
pub fn append_info_log(message: impl AsRef<str>) {
|
||||
append_cloud_log(CloudLogLevel::Info, message.as_ref());
|
||||
}
|
||||
|
||||
pub fn append_debug_log(message: impl AsRef<str>) {
|
||||
append_cloud_log(CloudLogLevel::Debug, message.as_ref());
|
||||
}
|
||||
|
||||
fn append_cloud_log(level: CloudLogLevel, message: &str) {
|
||||
if !should_log(level) {
|
||||
return;
|
||||
}
|
||||
let Some(path) = resolve_log_path() else {
|
||||
return;
|
||||
};
|
||||
|
||||
ensure_parent_dir(&path);
|
||||
rotate_log_file(&path, CLOUD_TASKS_LOG_MAX_BYTES, CLOUD_TASKS_LOG_BACKUPS);
|
||||
|
||||
let ts = Utc::now().to_rfc3339();
|
||||
let level_label = match level {
|
||||
CloudLogLevel::Error => "ERROR",
|
||||
CloudLogLevel::Info => "INFO",
|
||||
CloudLogLevel::Debug => "DEBUG",
|
||||
CloudLogLevel::Off => return,
|
||||
};
|
||||
let message = truncate_message(message, CLOUD_TASKS_LOG_MAX_MESSAGE_BYTES);
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
{
|
||||
use std::io::Write as _;
|
||||
let _ = writeln!(f, "[{ts}] {level_label} {message}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_user_agent_suffix(suffix: &str) {
|
||||
if let Ok(mut guard) = code_core::default_client::USER_AGENT_SUFFIX.lock() {
|
||||
@@ -8,18 +214,6 @@ pub fn set_user_agent_suffix(suffix: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_error_log(message: impl AsRef<str>) {
|
||||
let ts = Utc::now().to_rfc3339();
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("error.log")
|
||||
{
|
||||
use std::io::Write as _;
|
||||
let _ = writeln!(f, "[{ts}] {}", message.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize the configured base URL to a canonical form used by the backend client.
|
||||
/// - trims trailing '/'
|
||||
/// - appends '/backend-api' for ChatGPT hosts when missing
|
||||
|
||||
@@ -1490,10 +1490,69 @@ impl Config {
|
||||
|
||||
pub fn log_dir(cfg: &Config) -> std::io::Result<PathBuf> {
|
||||
let mut p = cfg.code_home.clone();
|
||||
p.push("log");
|
||||
p.push("debug_logs");
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
pub fn migrate_legacy_log_dirs(code_home: &Path) {
|
||||
let target = code_home.join("debug_logs");
|
||||
let legacy_dirs = [code_home.join("log"), code_home.join("logs")];
|
||||
for legacy in legacy_dirs {
|
||||
let _ = migrate_dir_contents(&legacy, &target);
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_dir_contents(source: &Path, target: &Path) -> std::io::Result<()> {
|
||||
if !source.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(target)?;
|
||||
for entry in std::fs::read_dir(source)? {
|
||||
let entry = entry?;
|
||||
migrate_entry(&entry.path(), target)?;
|
||||
}
|
||||
let _ = std::fs::remove_dir(source);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_entry(path: &Path, target_dir: &Path) -> std::io::Result<()> {
|
||||
let Some(file_name) = path.file_name() else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut target = target_dir.join(file_name);
|
||||
if target.exists() {
|
||||
let base = file_name.to_string_lossy();
|
||||
let mut idx = 1u32;
|
||||
loop {
|
||||
let candidate = target_dir.join(format!("{base}.legacy.{idx}"));
|
||||
if !candidate.exists() {
|
||||
target = candidate;
|
||||
break;
|
||||
}
|
||||
idx = idx.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
std::fs::create_dir_all(&target)?;
|
||||
for entry in std::fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
migrate_entry(&entry.path(), &target)?;
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match std::fs::rename(path, &target) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => {
|
||||
std::fs::copy(path, &target)?;
|
||||
std::fs::remove_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
@@ -1220,8 +1220,8 @@ fn compute_composer_rect(area: Rect, top_spacer_enabled: bool) -> Rect {
|
||||
if top_spacer_enabled {
|
||||
y_offset = y_offset.saturating_add(1);
|
||||
}
|
||||
let height = (area.height - y_offset)
|
||||
- BottomPane::BOTTOM_PAD_LINES.min((area.height - y_offset).saturating_sub(1));
|
||||
let available = area.height.saturating_sub(y_offset);
|
||||
let height = available - BottomPane::BOTTOM_PAD_LINES.min(available.saturating_sub(1));
|
||||
Rect {
|
||||
x: area.x + horizontal_padding,
|
||||
y: area.y + y_offset,
|
||||
|
||||
@@ -25821,10 +25821,10 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
|
||||
fn launch_chrome_with_profile(&mut self, port: u16) {
|
||||
use std::process::Stdio;
|
||||
let log_path = self.chrome_log_path();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let log_path = format!("{}/code-chrome.log", std::env::temp_dir().display());
|
||||
let mut cmd = std::process::Command::new(
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
);
|
||||
@@ -25838,12 +25838,10 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
|
||||
.arg("--disable-hang-monitor")
|
||||
.arg("--disable-background-timer-throttling")
|
||||
.arg("--enable-logging")
|
||||
.arg("--log-level=1")
|
||||
.arg(format!("--log-file={}", log_path))
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.stdin(Stdio::null());
|
||||
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
|
||||
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
|
||||
tracing::warn!("failed to launch Chrome with profile: {err}");
|
||||
}
|
||||
@@ -25851,7 +25849,6 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let log_path = format!("{}/code-chrome.log", std::env::temp_dir().display());
|
||||
let mut cmd = std::process::Command::new("google-chrome");
|
||||
cmd.arg(format!("--remote-debugging-port={}", port))
|
||||
.arg("--no-first-run")
|
||||
@@ -25863,12 +25860,10 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
|
||||
.arg("--disable-hang-monitor")
|
||||
.arg("--disable-background-timer-throttling")
|
||||
.arg("--enable-logging")
|
||||
.arg("--log-level=1")
|
||||
.arg(format!("--log-file={}", log_path))
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.stdin(Stdio::null());
|
||||
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
|
||||
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
|
||||
tracing::warn!("failed to launch Chrome with profile: {err}");
|
||||
}
|
||||
@@ -25876,7 +25871,6 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let log_path = format!("{}\\code-chrome.log", std::env::temp_dir().display());
|
||||
let chrome_paths = vec![
|
||||
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe".to_string(),
|
||||
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe".to_string(),
|
||||
@@ -25899,12 +25893,10 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
|
||||
.arg("--disable-hang-monitor")
|
||||
.arg("--disable-background-timer-throttling")
|
||||
.arg("--enable-logging")
|
||||
.arg("--log-level=1")
|
||||
.arg(format!("--log-file={}", log_path))
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.stdin(Stdio::null());
|
||||
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
|
||||
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
|
||||
tracing::warn!("failed to launch Chrome with profile: {err}");
|
||||
}
|
||||
@@ -25920,6 +25912,22 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
.update_status_text("using browser".to_string());
|
||||
}
|
||||
|
||||
fn chrome_log_path(&self) -> Option<String> {
|
||||
if !self.config.debug {
|
||||
return None;
|
||||
}
|
||||
let log_dir = code_core::config::log_dir(&self.config).ok()?;
|
||||
Some(log_dir.join("code-chrome.log").display().to_string())
|
||||
}
|
||||
|
||||
fn apply_chrome_logging(&self, cmd: &mut std::process::Command, log_path: Option<&str>) {
|
||||
if let Some(path) = log_path {
|
||||
cmd.arg("--enable-logging")
|
||||
.arg("--log-level=1")
|
||||
.arg(format!("--log-file={path}"));
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_to_chrome_after_launch(
|
||||
&mut self,
|
||||
port: u16,
|
||||
@@ -26467,10 +26475,10 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let profile_dir = temp_dir.join(format!("code-chrome-temp-{}", port));
|
||||
let log_path = self.chrome_log_path();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let log_path = format!("{}/code-chrome.log", std::env::temp_dir().display());
|
||||
let mut cmd = std::process::Command::new(
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
);
|
||||
@@ -26485,12 +26493,10 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
|
||||
.arg("--disable-hang-monitor")
|
||||
.arg("--disable-background-timer-throttling")
|
||||
.arg("--enable-logging")
|
||||
.arg("--log-level=1")
|
||||
.arg(format!("--log-file={}", log_path))
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.stdin(Stdio::null());
|
||||
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
|
||||
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
|
||||
tracing::warn!("failed to launch Chrome with temp profile: {err}");
|
||||
}
|
||||
@@ -26498,7 +26504,6 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let log_path = format!("{}/code-chrome.log", std::env::temp_dir().display());
|
||||
let mut cmd = std::process::Command::new("google-chrome");
|
||||
cmd.arg(format!("--remote-debugging-port={}", port))
|
||||
.arg(format!("--user-data-dir={}", profile_dir.display()))
|
||||
@@ -26511,12 +26516,10 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
|
||||
.arg("--disable-hang-monitor")
|
||||
.arg("--disable-background-timer-throttling")
|
||||
.arg("--enable-logging")
|
||||
.arg("--log-level=1")
|
||||
.arg(format!("--log-file={}", log_path))
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.stdin(Stdio::null());
|
||||
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
|
||||
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
|
||||
tracing::warn!("failed to launch Chrome with temp profile: {err}");
|
||||
}
|
||||
@@ -26524,7 +26527,6 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let log_path = format!("{}\\code-chrome.log", std::env::temp_dir().display());
|
||||
let chrome_paths = vec![
|
||||
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe".to_string(),
|
||||
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe".to_string(),
|
||||
@@ -26548,12 +26550,10 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
|
||||
.arg("--disable-hang-monitor")
|
||||
.arg("--disable-background-timer-throttling")
|
||||
.arg("--enable-logging")
|
||||
.arg("--log-level=1")
|
||||
.arg(format!("--log-file={}", log_path))
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.stdin(Stdio::null());
|
||||
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
|
||||
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
|
||||
tracing::warn!("failed to launch Chrome with temp profile: {err}");
|
||||
}
|
||||
|
||||
+74
-38
@@ -131,6 +131,41 @@ pub use cli::Cli;
|
||||
pub use self::markdown_render::render_markdown_text;
|
||||
pub use public_widgets::composer_input::{ComposerAction, ComposerInput};
|
||||
|
||||
const TUI_LOG_MAX_BYTES: u64 = 50 * 1024 * 1024;
|
||||
const TUI_LOG_BACKUPS: usize = 2;
|
||||
|
||||
fn rotate_log_file(log_dir: &Path, file_name: &str) {
|
||||
let path = log_dir.join(file_name);
|
||||
let Ok(meta) = std::fs::metadata(&path) else {
|
||||
return;
|
||||
};
|
||||
if meta.len() <= TUI_LOG_MAX_BYTES {
|
||||
return;
|
||||
}
|
||||
if TUI_LOG_BACKUPS == 0 {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
return;
|
||||
}
|
||||
|
||||
let oldest = log_dir.join(format!("{file_name}.{TUI_LOG_BACKUPS}"));
|
||||
let _ = std::fs::remove_file(&oldest);
|
||||
|
||||
if TUI_LOG_BACKUPS > 1 {
|
||||
for idx in (1..TUI_LOG_BACKUPS).rev() {
|
||||
let from = log_dir.join(format!("{file_name}.{idx}"));
|
||||
let to = log_dir.join(format!("{file_name}.{}", idx + 1));
|
||||
let _ = std::fs::rename(&from, &to);
|
||||
}
|
||||
}
|
||||
|
||||
let rotated = log_dir.join(format!("{file_name}.1"));
|
||||
if std::fs::rename(&path, rotated).is_err() {
|
||||
if let Ok(file) = std::fs::OpenOptions::new().write(true).open(&path) {
|
||||
let _ = file.set_len(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-helpers")]
|
||||
pub mod test_helpers {
|
||||
pub use crate::chatwidget::smoke_helpers::AutoContinueModeFixture;
|
||||
@@ -443,6 +478,8 @@ pub async fn run_main(
|
||||
}
|
||||
};
|
||||
|
||||
code_core::config::migrate_legacy_log_dirs(&code_home);
|
||||
|
||||
let housekeeping_home = code_home.clone();
|
||||
let housekeeping_handle = thread_spawner::spawn_lightweight("housekeeping", move || {
|
||||
if let Err(err) = code_core::run_housekeeping_if_due(&housekeeping_home) {
|
||||
@@ -604,52 +641,51 @@ pub async fn run_main(
|
||||
|
||||
let log_dir = code_core::config::log_dir(&config)?;
|
||||
std::fs::create_dir_all(&log_dir)?;
|
||||
// Open (or create) your log file, appending to it.
|
||||
let mut log_file_opts = OpenOptions::new();
|
||||
log_file_opts.create(true).append(true);
|
||||
|
||||
// Ensure the file is only readable and writable by the current user.
|
||||
// Doing the equivalent to `chmod 600` on Windows is quite a bit more code
|
||||
// and requires the Windows API crates, so we can reconsider that when
|
||||
// Code CLI is officially supported on Windows.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
log_file_opts.mode(0o600);
|
||||
}
|
||||
let (env_layer, _log_guard) = if cli.debug {
|
||||
rotate_log_file(&log_dir, "codex-tui.log");
|
||||
// Open (or create) your log file, appending to it.
|
||||
let mut log_file_opts = OpenOptions::new();
|
||||
log_file_opts.create(true).append(true);
|
||||
|
||||
let log_file = log_file_opts.open(log_dir.join("codex-tui.log"))?;
|
||||
// Ensure the file is only readable and writable by the current user.
|
||||
// Doing the equivalent to `chmod 600` on Windows is quite a bit more code
|
||||
// and requires the Windows API crates, so we can reconsider that when
|
||||
// Code CLI is officially supported on Windows.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
log_file_opts.mode(0o600);
|
||||
}
|
||||
|
||||
// Wrap file in non‑blocking writer.
|
||||
let (log_writer, _log_guard) = non_blocking(log_file);
|
||||
let log_file = log_file_opts.open(log_dir.join("codex-tui.log"))?;
|
||||
|
||||
let critical_dir = {
|
||||
let mut path = code_home.clone();
|
||||
path.push("logs");
|
||||
path
|
||||
// Wrap file in non‑blocking writer.
|
||||
let (log_writer, log_guard) = non_blocking(log_file);
|
||||
|
||||
let default_filter = "code_core=info,code_tui=info,code_browser=warn,code_auto_drive_core=info";
|
||||
|
||||
// use RUST_LOG env var, defaulting based on debug flag.
|
||||
let env_filter = || {
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter))
|
||||
};
|
||||
|
||||
let env_layer = tracing_subscriber::fmt::layer()
|
||||
.with_target(false)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
|
||||
.with_ansi(false)
|
||||
.with_writer(log_writer)
|
||||
.with_filter(env_filter());
|
||||
|
||||
(Some(env_layer), Some(log_guard))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
std::fs::create_dir_all(&critical_dir)?;
|
||||
|
||||
let critical_dir = log_dir.clone();
|
||||
let critical_appender = rolling::daily(&critical_dir, "critical.log");
|
||||
let (critical_writer, _critical_guard) = non_blocking(critical_appender);
|
||||
|
||||
let default_filter = if cli.debug {
|
||||
"code_core=info,code_tui=info,code_browser=info,code_auto_drive_core=info"
|
||||
} else {
|
||||
"code_core=warn,code_tui=warn,code_browser=warn"
|
||||
};
|
||||
|
||||
// use RUST_LOG env var, defaulting based on debug flag.
|
||||
let env_filter = || {
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter))
|
||||
};
|
||||
|
||||
let env_layer = tracing_subscriber::fmt::layer()
|
||||
.with_target(false)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
|
||||
.with_ansi(false)
|
||||
.with_writer(log_writer)
|
||||
.with_filter(env_filter());
|
||||
|
||||
let critical_layer = tracing_subscriber::fmt::layer()
|
||||
.with_target(false)
|
||||
.with_ansi(false)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::sync::OnceLock;
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
/// Sanitization mode:
|
||||
@@ -26,9 +27,31 @@ pub fn sanitize_for_tui(input: &str, mode: Mode, opts: Options) -> String {
|
||||
// with spaces we insert.
|
||||
let mut text = if opts.expand_tabs { expand_tabs_to_spaces(input, opts.tabstop) } else { input.to_string() };
|
||||
text = strip_specials(text, mode, opts.debug_markers);
|
||||
if ascii_only_enabled() {
|
||||
text = force_ascii(text);
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn ascii_only_enabled() -> bool {
|
||||
static ASCII_ONLY: OnceLock<bool> = OnceLock::new();
|
||||
*ASCII_ONLY.get_or_init(|| {
|
||||
std::env::var("CODEX_TUI_ASCII")
|
||||
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn force_ascii(input: String) -> String {
|
||||
if input.is_ascii() {
|
||||
return input;
|
||||
}
|
||||
input
|
||||
.chars()
|
||||
.map(|ch| if ch.is_ascii() { ch } else { '?' })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn expand_tabs_to_spaces(input: &str, tabstop: usize) -> String {
|
||||
let ts = tabstop.max(1);
|
||||
let mut out = String::with_capacity(input.len());
|
||||
|
||||
+7
-4
@@ -50,14 +50,14 @@ Notes:
|
||||
|
||||
Because Code is written in Rust, it honors the `RUST_LOG` environment variable to configure its logging behavior.
|
||||
|
||||
The TUI defaults to `RUST_LOG=code_core=info,code_tui=info,code_browser=info,code_auto_drive_core=info` and log messages are written to `~/.code/log/codex-tui.log` (Code still reads the legacy `~/.codex/log/` path), so you can leave the following running in a separate terminal to monitor log messages as they are written:
|
||||
When you run the TUI with `--debug`, log messages are written to `~/.code/debug_logs/codex-tui.log`, so you can leave the following running in a separate terminal to monitor log messages as they are written:
|
||||
|
||||
```
|
||||
tail -F ~/.code/log/codex-tui.log
|
||||
tail -F ~/.code/debug_logs/codex-tui.log
|
||||
```
|
||||
|
||||
When you also enable the CLI `--debug` flag, request/response JSON is
|
||||
partitioned into helper-specific folders under `~/.code/debug_logs/`. Expect
|
||||
When you enable the CLI `--debug` flag, request/response JSON is partitioned
|
||||
into helper-specific folders under `~/.code/debug_logs/`. Expect
|
||||
subdirectories such as:
|
||||
|
||||
- `auto/coordinator`
|
||||
@@ -74,6 +74,9 @@ subdirectories such as:
|
||||
Tags become nested path components, so custom helpers appear alongside the
|
||||
existing timestamped filenames.
|
||||
|
||||
Without `--debug`, Code only writes critical crash/error logs to
|
||||
`~/.code/debug_logs/critical.log.*`; routine log output is suppressed.
|
||||
|
||||
By comparison, the non-interactive mode (`code exec`) defaults to `RUST_LOG=error`, but messages are printed inline, so there is no need to monitor a separate file.
|
||||
|
||||
See the Rust documentation on [`RUST_LOG`](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) for more information on the configuration options.
|
||||
|
||||
Reference in New Issue
Block a user