mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
## What changed - Add an explicit `registered_core` provisioning mode that registers the app package for both managed sandbox accounts and records their runner aliases. Authenticate callers against the service's package family and installed version. - Persist runtime ownership and publish readiness only after both registrations and AppData metadata grants succeed. Prevent legacy setup or another owner from replacing registered sandbox accounts, and restrict `refresh_only` requests to an existing ready setup. - Scope service and pipe names by package identity. - Preserve registrations across package updates and clean them up when the owner uninstalls the app. Fence provisioning during teardown and defer package removal until native cleanup succeeds and the service exits. ## Testing Add tests for registration opt-in, ownership and readiness checks, legacy setup isolation, metadata ACL preservation, listener shutdown handling, and the cleanup finalizer's commit barrier and UTF-8 input. GitOrigin-RevId: 69e52f7463285b51f3bef831f071245f0eca0838
255 lines
8.7 KiB
Rust
255 lines
8.7 KiB
Rust
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
use std::os::windows::io::OwnedHandle;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::Mutex;
|
|
use std::sync::OnceLock;
|
|
|
|
use codex_utils_string::take_bytes_at_char_boundary;
|
|
use tracing_appender::rolling::RollingFileAppender;
|
|
use tracing_appender::rolling::Rotation;
|
|
|
|
const LOG_COMMAND_PREVIEW_LIMIT: usize = 200;
|
|
pub const LOG_FILE_PREFIX: &str = "sandbox";
|
|
pub const LOG_FILE_SUFFIX: &str = "log";
|
|
pub const MAX_LOG_FILES: usize = 90;
|
|
|
|
// Service provisioning never appends to a caller-controlled existing log entry.
|
|
// An initialized but empty slot also prevents falling back after an open failure.
|
|
static SETUP_LOG: OnceLock<Mutex<Option<(File, OwnedHandle)>>> = OnceLock::new();
|
|
|
|
fn exe_label() -> &'static str {
|
|
static LABEL: OnceLock<String> = OnceLock::new();
|
|
LABEL.get_or_init(|| {
|
|
std::env::current_exe()
|
|
.ok()
|
|
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
|
|
.unwrap_or_else(|| "proc".to_string())
|
|
})
|
|
}
|
|
|
|
fn preview(command: &[String]) -> String {
|
|
let joined = command.join(" ");
|
|
if joined.len() <= LOG_COMMAND_PREVIEW_LIMIT {
|
|
joined
|
|
} else {
|
|
take_bytes_at_char_boundary(&joined, LOG_COMMAND_PREVIEW_LIMIT).to_string()
|
|
}
|
|
}
|
|
|
|
pub fn log_file_path_for_utc_date(base_dir: &Path, date: chrono::NaiveDate) -> PathBuf {
|
|
base_dir.join(format!(
|
|
"{LOG_FILE_PREFIX}.{}.{}",
|
|
date.format("%Y-%m-%d"),
|
|
LOG_FILE_SUFFIX
|
|
))
|
|
}
|
|
|
|
pub fn current_log_file_path(base_dir: &Path) -> PathBuf {
|
|
log_file_path_for_utc_date(base_dir, chrono::Utc::now().date_naive())
|
|
}
|
|
|
|
pub fn current_log_file_path_for_codex_home(codex_home: &Path) -> PathBuf {
|
|
current_log_file_path(&crate::sandbox_dir(codex_home))
|
|
}
|
|
|
|
pub fn log_writer(base_dir: &Path) -> Option<RollingFileAppender> {
|
|
if !base_dir.is_dir() {
|
|
return None;
|
|
}
|
|
|
|
RollingFileAppender::builder()
|
|
.rotation(Rotation::DAILY)
|
|
.filename_prefix(LOG_FILE_PREFIX)
|
|
.filename_suffix(LOG_FILE_SUFFIX)
|
|
.max_log_files(MAX_LOG_FILES)
|
|
.build(base_dir)
|
|
.ok()
|
|
}
|
|
|
|
/// Opens one fresh log for service provisioning and retains its parent directory.
|
|
/// Subsequent setup diagnostics share this handle instead of reopening a pathname.
|
|
pub fn setup_log_writer(base_dir: &Path) -> Result<File> {
|
|
let mut log = SETUP_LOG
|
|
.get_or_init(|| Mutex::new(None))
|
|
.lock()
|
|
.map_err(|_| anyhow::anyhow!("setup log lock poisoned"))?;
|
|
match log.as_ref() {
|
|
Some((file, _parent)) => file.try_clone().context("clone setup log"),
|
|
None => {
|
|
let (file, parent) = crate::file_write::create_temporary_file(base_dir, ".log")?;
|
|
let writer = file.try_clone().context("clone setup log")?;
|
|
*log = Some((file, parent));
|
|
Ok(writer)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Release setup's retained handles under the setup lock, after its writers finish.
|
|
/// Keep the slot initialized so later diagnostics cannot reopen a caller-controlled path.
|
|
pub(crate) fn release_setup_log() -> Result<()> {
|
|
if let Some(log) = SETUP_LOG.get() {
|
|
let mut log = log
|
|
.lock()
|
|
.map_err(|_| anyhow::anyhow!("setup log lock poisoned"))?;
|
|
*log = None;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn append_line(line: &str, base_dir: Option<&Path>) {
|
|
if let Some(log) = SETUP_LOG.get() {
|
|
if let Ok(mut log) = log.lock()
|
|
&& let Some((file, _parent)) = log.as_mut()
|
|
{
|
|
let _ = writeln!(file, "{line}");
|
|
}
|
|
return;
|
|
}
|
|
if let Some(dir) = base_dir
|
|
&& let Some(mut f) = log_writer(dir)
|
|
{
|
|
let _ = writeln!(f, "{line}");
|
|
}
|
|
}
|
|
|
|
pub fn log_start(command: &[String], base_dir: Option<&Path>) {
|
|
let p = preview(command);
|
|
log_note(&format!("START: {p}"), base_dir);
|
|
}
|
|
|
|
pub fn log_success(command: &[String], base_dir: Option<&Path>) {
|
|
let p = preview(command);
|
|
log_note(&format!("SUCCESS: {p}"), base_dir);
|
|
}
|
|
|
|
pub fn log_failure(command: &[String], detail: &str, base_dir: Option<&Path>) {
|
|
let p = preview(command);
|
|
log_note(&format!("FAILURE: {p} ({detail})"), base_dir);
|
|
}
|
|
|
|
// Debug logging helper. Emits only when SBX_DEBUG=1 to avoid noisy logs.
|
|
pub fn debug_log(msg: &str, base_dir: Option<&Path>) {
|
|
if std::env::var("SBX_DEBUG").ok().as_deref() == Some("1") {
|
|
append_line(&format!("DEBUG: {msg}"), base_dir);
|
|
eprintln!("{msg}");
|
|
}
|
|
}
|
|
|
|
// Unconditional note logging to the daily sandbox log.
|
|
pub fn log_note(msg: &str, base_dir: Option<&Path>) {
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
append_line(&format!("[{ts} {}] {}", exe_label(), msg), base_dir);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn setup_log_releases_handles_without_path_fallback() {
|
|
const CHILD_ENV: &str = "CODEX_TEST_SETUP_LOG_RELEASE_CHILD";
|
|
if std::env::var(CHILD_ENV).ok().as_deref() != Some("1") {
|
|
// SETUP_LOG is process-global; isolate this test from ordinary daily logging.
|
|
let output = std::process::Command::new(std::env::current_exe().expect("test binary"))
|
|
.args([
|
|
"--exact",
|
|
"logging::tests::setup_log_releases_handles_without_path_fallback",
|
|
"--nocapture",
|
|
])
|
|
.env(CHILD_ENV, "1")
|
|
.output()
|
|
.expect("run isolated setup-log test");
|
|
assert!(output.status.success(), "isolated test failed: {output:?}");
|
|
assert!(
|
|
String::from_utf8_lossy(&output.stdout).contains("setup-log release verified"),
|
|
"isolated test did not run: {output:?}"
|
|
);
|
|
return;
|
|
}
|
|
|
|
let tempdir = tempfile::tempdir().expect("tempdir");
|
|
let directory = tempdir.path().join(".sandbox");
|
|
std::fs::create_dir(&directory).expect("create sandbox directory");
|
|
drop(setup_log_writer(&directory).expect("open retained setup log"));
|
|
let error =
|
|
std::fs::remove_dir_all(&directory).expect_err("setup handles prevent deletion");
|
|
assert_eq!(
|
|
error.raw_os_error(),
|
|
Some(windows_sys::Win32::Foundation::ERROR_SHARING_VIOLATION as i32)
|
|
);
|
|
|
|
release_setup_log().expect("release setup log");
|
|
std::fs::remove_dir_all(&directory).expect("delete sandbox after releasing handles");
|
|
std::fs::create_dir(&directory).expect("recreate caller-controlled directory");
|
|
release_setup_log().expect("repeated release is harmless");
|
|
log_note("must not reopen the directory", Some(&directory));
|
|
assert!(
|
|
std::fs::read_dir(&directory)
|
|
.expect("read directory")
|
|
.next()
|
|
.is_none()
|
|
);
|
|
println!("setup-log release verified");
|
|
}
|
|
|
|
#[test]
|
|
fn preview_does_not_panic_on_utf8_boundary() {
|
|
// Place a 4-byte emoji such that naive (byte-based) truncation would split it.
|
|
let prefix = "x".repeat(LOG_COMMAND_PREVIEW_LIMIT - 1);
|
|
let command = vec![format!("{prefix}😀")];
|
|
let result = std::panic::catch_unwind(|| preview(&command));
|
|
assert!(result.is_ok());
|
|
let previewed = result.unwrap();
|
|
assert!(previewed.len() <= LOG_COMMAND_PREVIEW_LIMIT);
|
|
}
|
|
|
|
#[test]
|
|
fn log_note_writes_to_daily_rolling_log() {
|
|
let tempdir = tempfile::tempdir().expect("tempdir");
|
|
|
|
log_note("hello daily log", Some(tempdir.path()));
|
|
|
|
let entries = std::fs::read_dir(tempdir.path())
|
|
.expect("read log dir")
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.expect("read entries");
|
|
assert_eq!(entries.len(), 1);
|
|
|
|
let log_path = entries[0].path();
|
|
let filename = log_path
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.expect("utf-8 filename");
|
|
assert!(filename.starts_with("sandbox."));
|
|
assert!(filename.ends_with(".log"));
|
|
|
|
let log = std::fs::read_to_string(log_path).expect("read log");
|
|
assert!(log.contains("hello daily log"));
|
|
}
|
|
|
|
#[test]
|
|
fn log_file_path_for_utc_date_matches_rolling_appender_name() {
|
|
let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 21).expect("valid date");
|
|
|
|
assert_eq!(
|
|
log_file_path_for_utc_date(Path::new("logs"), date),
|
|
PathBuf::from("logs").join("sandbox.2026-05-21.log")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn current_log_file_path_for_codex_home_uses_sandbox_dir() {
|
|
let codex_home = Path::new("codex-home");
|
|
|
|
assert_eq!(
|
|
current_log_file_path_for_codex_home(codex_home),
|
|
current_log_file_path(&codex_home.join(".sandbox"))
|
|
);
|
|
}
|
|
}
|