@@ -20,6 +20,8 @@ pub const ENV_LOG_LEVEL: &str = "CODETRACER_LOG_LEVEL";
2020pub const ENV_LOG_FILE : & str = "CODETRACER_LOG_FILE" ;
2121/// Environment variable enabling JSON error trailers on stderr.
2222pub const ENV_JSON_ERRORS : & str = "CODETRACER_JSON_ERRORS" ;
23+ /// Environment variable toggling IO capture strategies.
24+ pub const ENV_CAPTURE_IO : & str = "CODETRACER_CAPTURE_IO" ;
2325
2426static POLICY : OnceCell < RwLock < RecorderPolicy > > = OnceCell :: new ( ) ;
2527
@@ -61,6 +63,21 @@ impl FromStr for OnRecorderError {
6163 }
6264}
6365
66+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
67+ pub struct IoCapturePolicy {
68+ pub line_proxies : bool ,
69+ pub fd_fallback : bool ,
70+ }
71+
72+ impl Default for IoCapturePolicy {
73+ fn default ( ) -> Self {
74+ Self {
75+ line_proxies : true ,
76+ fd_fallback : false ,
77+ }
78+ }
79+ }
80+
6481/// Recorder-wide runtime configuration.
6582#[ derive( Debug , Clone , PartialEq , Eq ) ]
6683pub struct RecorderPolicy {
@@ -70,6 +87,7 @@ pub struct RecorderPolicy {
7087 pub log_level : Option < String > ,
7188 pub log_file : Option < PathBuf > ,
7289 pub json_errors : bool ,
90+ pub io_capture : IoCapturePolicy ,
7391}
7492
7593impl Default for RecorderPolicy {
@@ -81,6 +99,7 @@ impl Default for RecorderPolicy {
8199 log_level : None ,
82100 log_file : None ,
83101 json_errors : false ,
102+ io_capture : IoCapturePolicy :: default ( ) ,
84103 }
85104 }
86105}
@@ -111,6 +130,16 @@ impl RecorderPolicy {
111130 if let Some ( json_errors) = update. json_errors {
112131 self . json_errors = json_errors;
113132 }
133+ if let Some ( line_proxies) = update. io_capture_line_proxies {
134+ self . io_capture . line_proxies = line_proxies;
135+ if !self . io_capture . line_proxies {
136+ self . io_capture . fd_fallback = false ;
137+ }
138+ }
139+ if let Some ( fd_fallback) = update. io_capture_fd_fallback {
140+ // fd fallback requires proxies to be on.
141+ self . io_capture . fd_fallback = fd_fallback && self . io_capture . line_proxies ;
142+ }
114143 }
115144}
116145
@@ -130,6 +159,8 @@ struct PolicyUpdate {
130159 log_level : Option < String > ,
131160 log_file : Option < PolicyPath > ,
132161 json_errors : Option < bool > ,
162+ io_capture_line_proxies : Option < bool > ,
163+ io_capture_fd_fallback : Option < bool > ,
133164}
134165
135166/// Snapshot the current policy.
@@ -178,6 +209,12 @@ pub fn configure_policy_from_env() -> RecorderResult<()> {
178209 update. json_errors = Some ( parse_bool ( & value) ?) ;
179210 }
180211
212+ if let Ok ( value) = env:: var ( ENV_CAPTURE_IO ) {
213+ let ( line_proxies, fd_fallback) = parse_capture_io ( & value) ?;
214+ update. io_capture_line_proxies = Some ( line_proxies) ;
215+ update. io_capture_fd_fallback = Some ( fd_fallback) ;
216+ }
217+
181218 apply_policy_update ( update) ;
182219 Ok ( ( ) )
183220}
@@ -194,6 +231,54 @@ fn parse_bool(value: &str) -> RecorderResult<bool> {
194231 }
195232}
196233
234+ fn parse_capture_io ( value : & str ) -> RecorderResult < ( bool , bool ) > {
235+ let trimmed = value. trim ( ) ;
236+ if trimmed. is_empty ( ) {
237+ let default = IoCapturePolicy :: default ( ) ;
238+ return Ok ( ( default. line_proxies , default. fd_fallback ) ) ;
239+ }
240+
241+ let lower = trimmed. to_ascii_lowercase ( ) ;
242+ if matches ! (
243+ lower. as_str( ) ,
244+ "0" | "off" | "false" | "disable" | "disabled" | "none"
245+ ) {
246+ return Ok ( ( false , false ) ) ;
247+ }
248+ if matches ! ( lower. as_str( ) , "1" | "on" | "true" | "enable" | "enabled" ) {
249+ return Ok ( ( true , false ) ) ;
250+ }
251+
252+ let mut line_proxies = false ;
253+ let mut fd_fallback = false ;
254+ for token in lower. split ( ',' ) {
255+ match token. trim ( ) {
256+ "" => { }
257+ "proxies" | "proxy" => line_proxies = true ,
258+ "fd" | "mirror" | "fallback" => {
259+ line_proxies = true ;
260+ fd_fallback = true ;
261+ }
262+ other => {
263+ return Err ( usage ! (
264+ ErrorCode :: InvalidPolicyValue ,
265+ "invalid CODETRACER_CAPTURE_IO value '{}'" ,
266+ other
267+ ) ) ;
268+ }
269+ }
270+ }
271+
272+ if !line_proxies && !fd_fallback {
273+ return Err ( usage ! (
274+ ErrorCode :: InvalidPolicyValue ,
275+ "CODETRACER_CAPTURE_IO must enable at least 'proxies' or 'fd'"
276+ ) ) ;
277+ }
278+
279+ Ok ( ( line_proxies, fd_fallback) )
280+ }
281+
197282// === PyO3 helpers ===
198283
199284use pyo3:: prelude:: * ;
@@ -202,14 +287,16 @@ use pyo3::types::PyDict;
202287use crate :: ffi;
203288
204289#[ pyfunction( name = "configure_policy" ) ]
205- #[ pyo3( signature = ( on_recorder_error=None , require_trace=None , keep_partial_trace=None , log_level=None , log_file=None , json_errors=None ) ) ]
290+ #[ pyo3( signature = ( on_recorder_error=None , require_trace=None , keep_partial_trace=None , log_level=None , log_file=None , json_errors=None , io_capture_line_proxies= None , io_capture_fd_fallback= None ) ) ]
206291pub fn configure_policy_py (
207292 on_recorder_error : Option < & str > ,
208293 require_trace : Option < bool > ,
209294 keep_partial_trace : Option < bool > ,
210295 log_level : Option < & str > ,
211296 log_file : Option < & str > ,
212297 json_errors : Option < bool > ,
298+ io_capture_line_proxies : Option < bool > ,
299+ io_capture_fd_fallback : Option < bool > ,
213300) -> PyResult < ( ) > {
214301 let mut update = PolicyUpdate :: default ( ) ;
215302
@@ -245,6 +332,14 @@ pub fn configure_policy_py(
245332 update. json_errors = Some ( value) ;
246333 }
247334
335+ if let Some ( value) = io_capture_line_proxies {
336+ update. io_capture_line_proxies = Some ( value) ;
337+ }
338+
339+ if let Some ( value) = io_capture_fd_fallback {
340+ update. io_capture_fd_fallback = Some ( value) ;
341+ }
342+
248343 apply_policy_update ( update) ;
249344 Ok ( ( ) )
250345}
@@ -278,6 +373,11 @@ pub fn py_policy_snapshot(py: Python<'_>) -> PyResult<PyObject> {
278373 dict. set_item ( "log_file" , py. None ( ) ) ?;
279374 }
280375 dict. set_item ( "json_errors" , snapshot. json_errors ) ?;
376+
377+ let io_dict = PyDict :: new ( py) ;
378+ io_dict. set_item ( "line_proxies" , snapshot. io_capture . line_proxies ) ?;
379+ io_dict. set_item ( "fd_fallback" , snapshot. io_capture . fd_fallback ) ?;
380+ dict. set_item ( "io_capture" , io_dict) ?;
281381 Ok ( dict. into ( ) )
282382}
283383
@@ -301,6 +401,8 @@ mod tests {
301401 assert ! ( !snap. json_errors) ;
302402 assert ! ( snap. log_level. is_none( ) ) ;
303403 assert ! ( snap. log_file. is_none( ) ) ;
404+ assert ! ( snap. io_capture. line_proxies) ;
405+ assert ! ( !snap. io_capture. fd_fallback) ;
304406 }
305407
306408 #[ test]
@@ -313,6 +415,8 @@ mod tests {
313415 update. log_level = Some ( "debug" . to_string ( ) ) ;
314416 update. log_file = Some ( PolicyPath :: Value ( PathBuf :: from ( "/tmp/log.txt" ) ) ) ;
315417 update. json_errors = Some ( true ) ;
418+ update. io_capture_line_proxies = Some ( true ) ;
419+ update. io_capture_fd_fallback = Some ( true ) ;
316420
317421 apply_policy_update ( update) ;
318422
@@ -323,6 +427,8 @@ mod tests {
323427 assert_eq ! ( snap. log_level. as_deref( ) , Some ( "debug" ) ) ;
324428 assert_eq ! ( snap. log_file. as_deref( ) , Some ( Path :: new( "/tmp/log.txt" ) ) ) ;
325429 assert ! ( snap. json_errors) ;
430+ assert ! ( snap. io_capture. line_proxies) ;
431+ assert ! ( snap. io_capture. fd_fallback) ;
326432 reset_policy ( ) ;
327433 }
328434
@@ -336,6 +442,7 @@ mod tests {
336442 env:: set_var ( ENV_LOG_LEVEL , "info" ) ;
337443 env:: set_var ( ENV_LOG_FILE , "/tmp/out.log" ) ;
338444 env:: set_var ( ENV_JSON_ERRORS , "yes" ) ;
445+ env:: set_var ( ENV_CAPTURE_IO , "proxies,fd" ) ;
339446
340447 configure_policy_from_env ( ) . expect ( "configure from env" ) ;
341448
@@ -348,6 +455,8 @@ mod tests {
348455 assert_eq ! ( snap. log_level. as_deref( ) , Some ( "info" ) ) ;
349456 assert_eq ! ( snap. log_file. as_deref( ) , Some ( Path :: new( "/tmp/out.log" ) ) ) ;
350457 assert ! ( snap. json_errors) ;
458+ assert ! ( snap. io_capture. line_proxies) ;
459+ assert ! ( snap. io_capture. fd_fallback) ;
351460 reset_policy ( ) ;
352461 }
353462
@@ -364,6 +473,19 @@ mod tests {
364473 reset_policy ( ) ;
365474 }
366475
476+ #[ test]
477+ fn configure_policy_from_env_rejects_invalid_capture_io ( ) {
478+ reset_policy ( ) ;
479+ let env_guard = env_lock ( ) ;
480+ env:: set_var ( ENV_CAPTURE_IO , "invalid-token" ) ;
481+
482+ let err = configure_policy_from_env ( ) . expect_err ( "invalid capture io should error" ) ;
483+ assert_eq ! ( err. code, ErrorCode :: InvalidPolicyValue ) ;
484+
485+ drop ( env_guard) ;
486+ reset_policy ( ) ;
487+ }
488+
367489 fn env_lock ( ) -> EnvGuard {
368490 EnvGuard
369491 }
@@ -379,6 +501,7 @@ mod tests {
379501 ENV_LOG_LEVEL ,
380502 ENV_LOG_FILE ,
381503 ENV_JSON_ERRORS ,
504+ ENV_CAPTURE_IO ,
382505 ] {
383506 env:: remove_var ( key) ;
384507 }
0 commit comments