Compare commits

...

5 Commits

Author SHA1 Message Date
marina-oai
a40f62d647 Merge branch 'main' into codex/add-process-id-to-logging 2026-01-20 17:02:55 +09:00
marina-oai
3807353071 Merge branch 'main' into codex/add-process-id-to-logging 2026-01-19 11:34:57 +09:00
marina-oai
3bc5129982 Merge branch 'main' into codex/add-process-id-to-logging 2026-01-16 11:11:23 +09:00
marina-oai
d2628e8931 Merge branch 'main' into codex/add-process-id-to-logging 2026-01-15 17:19:23 +09:00
marina-oai
ca47abc69a Record exec pids in tool call spans 2026-01-15 17:16:24 +09:00
17 changed files with 151 additions and 33 deletions

View File

@@ -3871,6 +3871,7 @@ mod tests {
aggregated_output: StreamOutput::new("Command output".to_string()),
duration: StdDuration::from_secs(1),
timed_out: true,
os_pid: None,
};
let (_, turn_context) = make_session_and_context().await;

View File

@@ -636,6 +636,7 @@ mod tests {
aggregated_output: StreamOutput::new("aggregate detail".to_string()),
duration: Duration::from_millis(10),
timed_out: false,
os_pid: None,
};
let err = CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(output),
@@ -652,6 +653,7 @@ mod tests {
aggregated_output: StreamOutput::new(String::new()),
duration: Duration::from_millis(10),
timed_out: false,
os_pid: None,
};
let err = CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(output),
@@ -668,6 +670,7 @@ mod tests {
aggregated_output: StreamOutput::new(String::new()),
duration: Duration::from_millis(8),
timed_out: false,
os_pid: None,
};
let err = CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(output),
@@ -711,6 +714,7 @@ mod tests {
aggregated_output: StreamOutput::new(String::new()),
duration: Duration::from_millis(5),
timed_out: false,
os_pid: None,
};
let err = CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(output),

View File

@@ -313,6 +313,7 @@ async fn exec_windows_sandbox(
stderr,
aggregated_output,
timed_out: capture.timed_out,
os_pid: None,
})
}
@@ -352,6 +353,7 @@ fn finalize_exec_result(
aggregated_output,
duration,
timed_out,
os_pid: raw_output.os_pid,
};
if timed_out {
@@ -469,6 +471,7 @@ struct RawExecToolCallOutput {
pub stderr: StreamOutput<Vec<u8>>,
pub aggregated_output: StreamOutput<Vec<u8>>,
pub timed_out: bool,
pub os_pid: Option<u32>,
}
impl StreamOutput<String> {
@@ -502,6 +505,7 @@ pub struct ExecToolCallOutput {
pub aggregated_output: StreamOutput<String>,
pub duration: Duration,
pub timed_out: bool,
pub os_pid: Option<u32>,
}
impl Default for ExecToolCallOutput {
@@ -513,6 +517,7 @@ impl Default for ExecToolCallOutput {
aggregated_output: StreamOutput::new(String::new()),
duration: Duration::ZERO,
timed_out: false,
os_pid: None,
}
}
}
@@ -573,6 +578,7 @@ async fn consume_truncated_output(
// above, therefore `take()` should normally return `Some`. If it doesn't
// we treat it as an exceptional I/O error
let os_pid = child.id();
let stdout_reader = child.stdout.take().ok_or_else(|| {
CodexErr::Io(io::Error::other(
"stdout pipe was unexpectedly not available",
@@ -680,6 +686,7 @@ async fn consume_truncated_output(
stderr,
aggregated_output,
timed_out,
os_pid,
})
}
@@ -769,6 +776,7 @@ mod tests {
aggregated_output: StreamOutput::new(aggregated.to_string()),
duration: Duration::from_millis(1),
timed_out: false,
os_pid: None,
}
}

View File

@@ -90,6 +90,7 @@ impl SessionTask for UserShellCommandTask {
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: call_id.clone(),
process_id: None,
os_pid: None,
turn_id: turn_context.sub_id.clone(),
command: command.clone(),
cwd: cwd.clone(),
@@ -134,6 +135,7 @@ impl SessionTask for UserShellCommandTask {
aggregated_output: StreamOutput::new(aborted_message.clone()),
duration: Duration::ZERO,
timed_out: false,
os_pid: None,
};
let output_items = [user_shell_command_record_item(
&raw_command,
@@ -149,6 +151,7 @@ impl SessionTask for UserShellCommandTask {
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id,
process_id: None,
os_pid: None,
turn_id: turn_context.sub_id.clone(),
command: command.clone(),
cwd: cwd.clone(),
@@ -172,6 +175,7 @@ impl SessionTask for UserShellCommandTask {
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: call_id.clone(),
process_id: None,
os_pid: output.os_pid,
turn_id: turn_context.sub_id.clone(),
command: command.clone(),
cwd: cwd.clone(),
@@ -210,6 +214,7 @@ impl SessionTask for UserShellCommandTask {
aggregated_output: StreamOutput::new(message.clone()),
duration: Duration::ZERO,
timed_out: false,
os_pid: None,
};
session
.send_event(
@@ -217,6 +222,7 @@ impl SessionTask for UserShellCommandTask {
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id,
process_id: None,
os_pid: None,
turn_id: turn_context.sub_id.clone(),
command,
cwd,

View File

@@ -58,6 +58,7 @@ pub(crate) enum ToolEventFailure {
Message(String),
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn emit_exec_command_begin(
ctx: ToolEventCtx<'_>,
command: &[String],
@@ -66,21 +67,22 @@ pub(crate) async fn emit_exec_command_begin(
source: ExecCommandSource,
interaction_input: Option<String>,
process_id: Option<&str>,
os_pid: Option<u32>,
) {
let event = ExecCommandBeginEvent {
call_id: ctx.call_id.to_string(),
process_id: process_id.map(str::to_owned),
os_pid,
turn_id: ctx.turn.sub_id.clone(),
command: command.to_vec(),
cwd: cwd.to_path_buf(),
parsed_cmd: parsed_cmd.to_vec(),
source,
interaction_input,
};
record_exec_span_metadata(process_id, os_pid);
ctx.session
.send_event(
ctx.turn,
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: ctx.call_id.to_string(),
process_id: process_id.map(str::to_owned),
turn_id: ctx.turn.sub_id.clone(),
command: command.to_vec(),
cwd: cwd.to_path_buf(),
parsed_cmd: parsed_cmd.to_vec(),
source,
interaction_input,
}),
)
.send_event(ctx.turn, EventMsg::ExecCommandBegin(event))
.await;
}
// Concrete, allocation-free emitter: avoid trait objects and boxed futures.
@@ -102,6 +104,7 @@ pub(crate) enum ToolEmitter {
source: ExecCommandSource,
parsed_cmd: Vec<ParsedCommand>,
process_id: Option<String>,
os_pid: Option<u32>,
},
}
@@ -134,6 +137,7 @@ impl ToolEmitter {
cwd: PathBuf,
source: ExecCommandSource,
process_id: Option<String>,
os_pid: Option<u32>,
) -> Self {
let parsed_cmd = parse_command(command);
Self::UnifiedExec {
@@ -142,6 +146,7 @@ impl ToolEmitter {
source,
parsed_cmd,
process_id,
os_pid,
}
}
@@ -159,7 +164,15 @@ impl ToolEmitter {
) => {
emit_exec_stage(
ctx,
ExecCommandInput::new(command, cwd.as_path(), parsed_cmd, *source, None, None),
ExecCommandInput::new(
command,
cwd.as_path(),
parsed_cmd,
*source,
None,
None,
None,
),
stage,
)
.await;
@@ -231,6 +244,7 @@ impl ToolEmitter {
source,
parsed_cmd,
process_id,
os_pid,
},
stage,
) => {
@@ -243,6 +257,7 @@ impl ToolEmitter {
*source,
None,
process_id.as_deref(),
*os_pid,
),
stage,
)
@@ -328,6 +343,7 @@ struct ExecCommandInput<'a> {
source: ExecCommandSource,
interaction_input: Option<&'a str>,
process_id: Option<&'a str>,
os_pid: Option<u32>,
}
impl<'a> ExecCommandInput<'a> {
@@ -338,6 +354,7 @@ impl<'a> ExecCommandInput<'a> {
source: ExecCommandSource,
interaction_input: Option<&'a str>,
process_id: Option<&'a str>,
os_pid: Option<u32>,
) -> Self {
Self {
command,
@@ -346,6 +363,7 @@ impl<'a> ExecCommandInput<'a> {
source,
interaction_input,
process_id,
os_pid,
}
}
}
@@ -357,6 +375,7 @@ struct ExecCommandResult {
exit_code: i32,
duration: Duration,
formatted_output: String,
os_pid: Option<u32>,
}
async fn emit_exec_stage(
@@ -374,11 +393,13 @@ async fn emit_exec_stage(
exec_input.source,
exec_input.interaction_input.map(str::to_owned),
exec_input.process_id,
exec_input.os_pid,
)
.await;
}
ToolEventStage::Success(output)
| ToolEventStage::Failure(ToolEventFailure::Output(output)) => {
let os_pid = exec_input.os_pid.or(output.os_pid);
let exec_result = ExecCommandResult {
stdout: output.stdout.text.clone(),
stderr: output.stderr.text.clone(),
@@ -386,6 +407,7 @@ async fn emit_exec_stage(
exit_code: output.exit_code,
duration: output.duration,
formatted_output: format_exec_output_str(&output, ctx.turn.truncation_policy),
os_pid,
};
emit_exec_end(ctx, exec_input, exec_result).await;
}
@@ -398,6 +420,7 @@ async fn emit_exec_stage(
exit_code: -1,
duration: Duration::ZERO,
formatted_output: text,
os_pid: exec_input.os_pid,
};
emit_exec_end(ctx, exec_input, exec_result).await;
}
@@ -409,29 +432,45 @@ async fn emit_exec_end(
exec_input: ExecCommandInput<'_>,
exec_result: ExecCommandResult,
) {
let event = ExecCommandEndEvent {
call_id: ctx.call_id.to_string(),
process_id: exec_input.process_id.map(str::to_owned),
os_pid: exec_result.os_pid,
turn_id: ctx.turn.sub_id.clone(),
command: exec_input.command.to_vec(),
cwd: exec_input.cwd.to_path_buf(),
parsed_cmd: exec_input.parsed_cmd.to_vec(),
source: exec_input.source,
interaction_input: exec_input.interaction_input.map(str::to_owned),
stdout: exec_result.stdout,
stderr: exec_result.stderr,
aggregated_output: exec_result.aggregated_output,
exit_code: exec_result.exit_code,
duration: exec_result.duration,
formatted_output: exec_result.formatted_output,
};
record_exec_span_metadata(
exec_input.process_id,
exec_result.os_pid.or(exec_input.os_pid),
);
ctx.session
.send_event(
ctx.turn,
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: ctx.call_id.to_string(),
process_id: exec_input.process_id.map(str::to_owned),
turn_id: ctx.turn.sub_id.clone(),
command: exec_input.command.to_vec(),
cwd: exec_input.cwd.to_path_buf(),
parsed_cmd: exec_input.parsed_cmd.to_vec(),
source: exec_input.source,
interaction_input: exec_input.interaction_input.map(str::to_owned),
stdout: exec_result.stdout,
stderr: exec_result.stderr,
aggregated_output: exec_result.aggregated_output,
exit_code: exec_result.exit_code,
duration: exec_result.duration,
formatted_output: exec_result.formatted_output,
}),
)
.send_event(ctx.turn, EventMsg::ExecCommandEnd(event))
.await;
}
fn record_exec_span_metadata(process_id: Option<&str>, os_pid: Option<u32>) {
let span = tracing::Span::current();
if span.is_disabled() {
return;
}
if let Some(process_id) = process_id {
span.record("process_id", process_id);
}
if let Some(os_pid) = os_pid {
span.record("os_pid", os_pid);
}
}
async fn emit_patch_end(
ctx: ToolEventCtx<'_>,
changes: HashMap<PathBuf, FileChange>,

View File

@@ -6,6 +6,7 @@ use tokio_util::either::Either;
use tokio_util::sync::CancellationToken;
use tokio_util::task::AbortOnDropHandle;
use tracing::Instrument;
use tracing::field;
use tracing::instrument;
use tracing::trace_span;
@@ -66,6 +67,8 @@ impl ToolCallRuntime {
tool_name = call.tool_name.as_str(),
call_id = call.call_id.as_str(),
aborted = false,
process_id = field::Empty,
os_pid = field::Empty,
);
let handle: AbortOnDropHandle<Result<ResponseInputItem, FunctionCallError>> =

View File

@@ -111,6 +111,7 @@ pub(crate) fn spawn_exit_watcher(
command: Vec<String>,
cwd: PathBuf,
process_id: String,
os_pid: Option<u32>,
transcript: Arc<Mutex<HeadTailBuffer>>,
started_at: Instant,
) {
@@ -130,6 +131,7 @@ pub(crate) fn spawn_exit_watcher(
command,
cwd,
Some(process_id),
os_pid,
transcript,
String::new(),
exit_code,
@@ -182,6 +184,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
command: Vec<String>,
cwd: PathBuf,
process_id: Option<String>,
os_pid: Option<u32>,
transcript: Arc<Mutex<HeadTailBuffer>>,
fallback_output: String,
exit_code: i32,
@@ -195,6 +198,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
aggregated_output: StreamOutput::new(aggregated_output),
duration,
timed_out: false,
os_pid,
};
let event_ctx = ToolEventCtx::new(session_ref.as_ref(), turn_ref.as_ref(), &call_id, None);
let emitter = ToolEmitter::unified_exec(
@@ -202,6 +206,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
cwd,
ExecCommandSource::UnifiedExecStartup,
process_id,
os_pid,
);
emitter
.emit(event_ctx, ToolEventStage::Success(output))

View File

@@ -95,6 +95,10 @@ impl UnifiedExecProcess {
self.process_handle.output_receiver()
}
pub(crate) fn os_pid(&self) -> Option<u32> {
self.process_handle.os_pid()
}
pub(super) fn cancellation_token(&self) -> CancellationToken {
self.cancellation_token.clone()
}
@@ -156,6 +160,7 @@ impl UnifiedExecProcess {
exit_code,
stderr: StreamOutput::new(text.to_string()),
aggregated_output: StreamOutput::new(text.to_string()),
os_pid: self.os_pid(),
..Default::default()
};
if is_likely_sandbox_denied(sandbox_type, &exec_output) {

View File

@@ -142,6 +142,7 @@ impl UnifiedExecProcessManager {
};
let transcript = Arc::new(tokio::sync::Mutex::new(HeadTailBuffer::default()));
let os_pid = process.os_pid();
let event_ctx = ToolEventCtx::new(
context.session.as_ref(),
context.turn.as_ref(),
@@ -153,6 +154,7 @@ impl UnifiedExecProcessManager {
cwd.clone(),
ExecCommandSource::UnifiedExecStartup,
Some(request.process_id.clone()),
os_pid,
);
emitter.emit(event_ctx, ToolEventStage::Begin).await;
@@ -198,6 +200,7 @@ impl UnifiedExecProcessManager {
request.command.clone(),
cwd,
Some(process_id),
os_pid,
Arc::clone(&transcript),
output.clone(),
exit,
@@ -219,6 +222,7 @@ impl UnifiedExecProcessManager {
cwd.clone(),
start,
process_id,
os_pid,
request.tty,
Arc::clone(&transcript),
)
@@ -409,6 +413,7 @@ impl UnifiedExecProcessManager {
cwd: PathBuf,
started_at: Instant,
process_id: String,
os_pid: Option<u32>,
tty: bool,
transcript: Arc<tokio::sync::Mutex<HeadTailBuffer>>,
) {
@@ -445,6 +450,7 @@ impl UnifiedExecProcessManager {
command.to_vec(),
cwd,
process_id,
os_pid,
transcript,
started_at,
);

View File

@@ -89,6 +89,7 @@ mod tests {
aggregated_output: StreamOutput::new("hi".to_string()),
duration: Duration::from_secs(1),
timed_out: false,
os_pid: None,
};
let (_, turn_context) = make_session_and_context().await;
let item = user_shell_command_record_item("echo hi", &exec_output, &turn_context);
@@ -113,6 +114,7 @@ mod tests {
aggregated_output: StreamOutput::new("combined output wins".to_string()),
duration: Duration::from_millis(120),
timed_out: false,
os_pid: None,
};
let (_, turn_context) = make_session_and_context().await;
let record = format_user_shell_command_record("false", &exec_output, &turn_context);

View File

@@ -641,6 +641,7 @@ fn exec_command_end_success_produces_completed_command_item() {
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "1".to_string(),
process_id: None,
os_pid: None,
turn_id: "turn-1".to_string(),
command: command.clone(),
cwd: cwd.clone(),
@@ -671,6 +672,7 @@ fn exec_command_end_success_produces_completed_command_item() {
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "1".to_string(),
process_id: None,
os_pid: None,
turn_id: "turn-1".to_string(),
command,
cwd,
@@ -718,6 +720,7 @@ fn command_execution_output_delta_updates_item_progress() {
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "delta-1".to_string(),
process_id: Some("42".to_string()),
os_pid: None,
turn_id: "turn-1".to_string(),
command: command.clone(),
cwd: cwd.clone(),
@@ -758,6 +761,7 @@ fn command_execution_output_delta_updates_item_progress() {
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "delta-1".to_string(),
process_id: Some("42".to_string()),
os_pid: None,
turn_id: "turn-1".to_string(),
command,
cwd,
@@ -802,6 +806,7 @@ fn exec_command_end_failure_produces_failed_command_item() {
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "2".to_string(),
process_id: None,
os_pid: None,
turn_id: "turn-1".to_string(),
command: command.clone(),
cwd: cwd.clone(),
@@ -831,6 +836,7 @@ fn exec_command_end_failure_produces_failed_command_item() {
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "2".to_string(),
process_id: None,
os_pid: None,
turn_id: "turn-1".to_string(),
command,
cwd,
@@ -872,6 +878,7 @@ fn exec_command_end_without_begin_is_ignored() {
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "no-begin".to_string(),
process_id: None,
os_pid: None,
turn_id: "turn-1".to_string(),
command: Vec::new(),
cwd: PathBuf::from("."),

View File

@@ -1749,6 +1749,10 @@ pub struct ExecCommandBeginEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub process_id: Option<String>,
/// OS process identifier for the spawned command (when available).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub os_pid: Option<u32>,
/// Turn ID that this command belongs to.
pub turn_id: String,
/// The command to be executed.
@@ -1773,6 +1777,10 @@ pub struct ExecCommandEndEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub process_id: Option<String>,
/// OS process identifier for the spawned command (when available).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub os_pid: Option<u32>,
/// Turn ID that this command belongs to.
pub turn_id: String,
/// The command that was executed.

View File

@@ -1270,6 +1270,7 @@ fn begin_exec_with_source(
let event = ExecCommandBeginEvent {
call_id: call_id.to_string(),
process_id: None,
os_pid: None,
turn_id: "turn-1".to_string(),
command,
cwd,
@@ -1295,6 +1296,7 @@ fn begin_unified_exec_startup(
let event = ExecCommandBeginEvent {
call_id: call_id.to_string(),
process_id: Some(process_id.to_string()),
os_pid: None,
turn_id: "turn-1".to_string(),
command,
cwd,
@@ -1345,12 +1347,14 @@ fn end_exec(
source,
interaction_input,
process_id,
os_pid,
} = begin_event;
chat.handle_codex_event(Event {
id: call_id.clone(),
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id,
process_id,
os_pid,
turn_id,
command,
cwd,
@@ -1624,6 +1628,7 @@ async fn exec_end_without_begin_uses_event_command() {
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "call-orphan".to_string(),
process_id: None,
os_pid: None,
turn_id: "turn-1".to_string(),
command,
cwd,
@@ -4213,6 +4218,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "c1".into(),
process_id: None,
os_pid: None,
turn_id: "turn-1".into(),
command: command.clone(),
cwd: cwd.clone(),
@@ -4226,6 +4232,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "c1".into(),
process_id: None,
os_pid: None,
turn_id: "turn-1".into(),
command,
cwd,

View File

@@ -1255,6 +1255,7 @@ fn begin_exec_with_source(
let event = ExecCommandBeginEvent {
call_id: call_id.to_string(),
process_id: None,
os_pid: None,
turn_id: "turn-1".to_string(),
command,
cwd,
@@ -1294,12 +1295,14 @@ fn end_exec(
source,
interaction_input,
process_id,
os_pid,
} = begin_event;
chat.handle_codex_event(Event {
id: call_id.clone(),
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id,
process_id,
os_pid,
turn_id,
command,
cwd,
@@ -1576,6 +1579,7 @@ async fn exec_end_without_begin_uses_event_command() {
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "call-orphan".to_string(),
process_id: None,
os_pid: None,
turn_id: "turn-1".to_string(),
command,
cwd,
@@ -3797,6 +3801,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "c1".into(),
process_id: None,
os_pid: None,
turn_id: "turn-1".into(),
command: command.clone(),
cwd: cwd.clone(),
@@ -3810,6 +3815,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "c1".into(),
process_id: None,
os_pid: None,
turn_id: "turn-1".into(),
command,
cwd,

View File

@@ -223,6 +223,7 @@ async fn spawn_process_with_stdin_mode(
writer_tx,
output_tx,
initial_output_rx,
Some(pid),
Box::new(PipeChildTerminator {
#[cfg(windows)]
pid,

View File

@@ -31,6 +31,7 @@ impl fmt::Debug for PtyHandles {
pub struct ProcessHandle {
writer_tx: mpsc::Sender<Vec<u8>>,
output_tx: broadcast::Sender<Vec<u8>>,
os_pid: Option<u32>,
killer: StdMutex<Option<Box<dyn ChildTerminator>>>,
reader_handle: StdMutex<Option<JoinHandle<()>>>,
reader_abort_handles: StdMutex<Vec<AbortHandle>>,
@@ -55,6 +56,7 @@ impl ProcessHandle {
writer_tx: mpsc::Sender<Vec<u8>>,
output_tx: broadcast::Sender<Vec<u8>>,
initial_output_rx: broadcast::Receiver<Vec<u8>>,
os_pid: Option<u32>,
killer: Box<dyn ChildTerminator>,
reader_handle: JoinHandle<()>,
reader_abort_handles: Vec<AbortHandle>,
@@ -68,6 +70,7 @@ impl ProcessHandle {
Self {
writer_tx,
output_tx,
os_pid,
killer: StdMutex::new(Some(killer)),
reader_handle: StdMutex::new(Some(reader_handle)),
reader_abort_handles: StdMutex::new(reader_abort_handles),
@@ -91,6 +94,11 @@ impl ProcessHandle {
self.output_tx.subscribe()
}
/// Process identifier reported by the underlying OS, if available.
pub fn os_pid(&self) -> Option<u32> {
self.os_pid
}
/// True if the child process has exited.
pub fn has_exited(&self) -> bool {
self.exit_status.load(std::sync::atomic::Ordering::SeqCst)

View File

@@ -86,6 +86,7 @@ pub async fn spawn_process(
}
let mut child = pair.slave.spawn_command(command_builder)?;
let os_pid = child.process_id();
let killer = child.clone_killer();
let (writer_tx, mut writer_rx) = mpsc::channel::<Vec<u8>>(128);
@@ -156,6 +157,7 @@ pub async fn spawn_process(
writer_tx,
output_tx,
initial_output_rx,
os_pid,
Box::new(PtyChildTerminator { killer }),
reader_handle,
Vec::new(),