Fix ACP connection and thread leak (#35670)
When you switched away from an ACP thread, the `AcpThreadView` entity (and thus thread, and subprocess) was leaked. This happened because we were using `cx.processor` for the `list` state callback, which uses a strong reference. This PR changes the callback so that it holds a weak reference, and adds some tests and assertions at various levels to make sure we don't reintroduce the leak in the future. Release Notes: - N/A
This commit is contained in:
parent
f27dc7dec7
commit
b7469f5bc3
8 changed files with 73 additions and 27 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -137,9 +137,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol"
|
name = "agent-client-protocol"
|
||||||
version = "0.0.18"
|
version = "0.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8e4c1dccb35e69d32566f0d11948d902f9942fc3f038821816c1150cf5925f4"
|
checksum = "12dbfec3d27680337ed9d3064eecafe97acf0b0f190148bb4e29d96707c9e403"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
|
|
|
@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||||
#
|
#
|
||||||
|
|
||||||
agentic-coding-protocol = "0.0.10"
|
agentic-coding-protocol = "0.0.10"
|
||||||
agent-client-protocol = "0.0.18"
|
agent-client-protocol = "0.0.20"
|
||||||
aho-corasick = "1.1"
|
aho-corasick = "1.1"
|
||||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||||
any_vec = "0.14"
|
any_vec = "0.14"
|
||||||
|
|
|
@ -380,6 +380,7 @@ impl AcpConnection {
|
||||||
|
|
||||||
let stdin = child.stdin.take().unwrap();
|
let stdin = child.stdin.take().unwrap();
|
||||||
let stdout = child.stdout.take().unwrap();
|
let stdout = child.stdout.take().unwrap();
|
||||||
|
log::trace!("Spawned (pid: {})", child.id());
|
||||||
|
|
||||||
let foreground_executor = cx.foreground_executor().clone();
|
let foreground_executor = cx.foreground_executor().clone();
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ pub struct AcpConnection {
|
||||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||||
auth_methods: Vec<acp::AuthMethod>,
|
auth_methods: Vec<acp::AuthMethod>,
|
||||||
_io_task: Task<Result<()>>,
|
_io_task: Task<Result<()>>,
|
||||||
_child: smol::process::Child,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AcpSession {
|
pub struct AcpSession {
|
||||||
|
@ -47,6 +46,7 @@ impl AcpConnection {
|
||||||
|
|
||||||
let stdout = child.stdout.take().expect("Failed to take stdout");
|
let stdout = child.stdout.take().expect("Failed to take stdout");
|
||||||
let stdin = child.stdin.take().expect("Failed to take stdin");
|
let stdin = child.stdin.take().expect("Failed to take stdin");
|
||||||
|
log::trace!("Spawned (pid: {})", child.id());
|
||||||
|
|
||||||
let sessions = Rc::new(RefCell::new(HashMap::default()));
|
let sessions = Rc::new(RefCell::new(HashMap::default()));
|
||||||
|
|
||||||
|
@ -61,7 +61,11 @@ impl AcpConnection {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let io_task = cx.background_spawn(io_task);
|
let io_task = cx.background_spawn(async move {
|
||||||
|
io_task.await?;
|
||||||
|
drop(child);
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
let response = connection
|
let response = connection
|
||||||
.initialize(acp::InitializeRequest {
|
.initialize(acp::InitializeRequest {
|
||||||
|
@ -84,7 +88,6 @@ impl AcpConnection {
|
||||||
connection: connection.into(),
|
connection: connection.into(),
|
||||||
server_name,
|
server_name,
|
||||||
sessions,
|
sessions,
|
||||||
_child: child,
|
|
||||||
_io_task: io_task,
|
_io_task: io_task,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -155,8 +158,10 @@ impl AgentConnection for AcpConnection {
|
||||||
|
|
||||||
fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task<Result<()>> {
|
fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task<Result<()>> {
|
||||||
let conn = self.connection.clone();
|
let conn = self.connection.clone();
|
||||||
cx.foreground_executor()
|
cx.foreground_executor().spawn(async move {
|
||||||
.spawn(async move { Ok(conn.prompt(params).await?) })
|
conn.prompt(params).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||||
|
|
|
@ -125,8 +125,7 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
&mcp_config_path,
|
&mcp_config_path,
|
||||||
&cwd,
|
&cwd,
|
||||||
)
|
)?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let pid = child.id();
|
let pid = child.id();
|
||||||
log::trace!("Spawned (pid: {})", pid);
|
log::trace!("Spawned (pid: {})", pid);
|
||||||
|
@ -262,7 +261,7 @@ enum ClaudeSessionMode {
|
||||||
Resume,
|
Resume,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn spawn_claude(
|
fn spawn_claude(
|
||||||
command: &AgentServerCommand,
|
command: &AgentServerCommand,
|
||||||
mode: ClaudeSessionMode,
|
mode: ClaudeSessionMode,
|
||||||
session_id: acp::SessionId,
|
session_id: acp::SessionId,
|
||||||
|
|
|
@ -311,6 +311,27 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||||
|
let fs = init_test(cx).await;
|
||||||
|
let project = Project::test(fs, [], cx).await;
|
||||||
|
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||||
|
|
||||||
|
thread
|
||||||
|
.update(cx, |thread, cx| thread.send_raw("Hello from test!", cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
thread.read_with(cx, |thread, _| {
|
||||||
|
assert!(thread.entries().len() >= 2, "Expected at least 2 entries");
|
||||||
|
});
|
||||||
|
|
||||||
|
let weak_thread = thread.downgrade();
|
||||||
|
drop(thread);
|
||||||
|
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
assert!(!weak_thread.is_upgradable());
|
||||||
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! common_e2e_tests {
|
macro_rules! common_e2e_tests {
|
||||||
($server:expr, allow_option_id = $allow_option_id:expr) => {
|
($server:expr, allow_option_id = $allow_option_id:expr) => {
|
||||||
|
@ -351,6 +372,12 @@ macro_rules! common_e2e_tests {
|
||||||
async fn cancel(cx: &mut ::gpui::TestAppContext) {
|
async fn cancel(cx: &mut ::gpui::TestAppContext) {
|
||||||
$crate::e2e_tests::test_cancel($server, cx).await;
|
$crate::e2e_tests::test_cancel($server, cx).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[::gpui::test]
|
||||||
|
#[cfg_attr(not(feature = "e2e"), ignore)]
|
||||||
|
async fn thread_drop(cx: &mut ::gpui::TestAppContext) {
|
||||||
|
$crate::e2e_tests::test_thread_drop($server, cx).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,12 +169,13 @@ impl AcpThreadView {
|
||||||
|
|
||||||
let mention_set = mention_set.clone();
|
let mention_set = mention_set.clone();
|
||||||
|
|
||||||
let list_state = ListState::new(
|
let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0), {
|
||||||
0,
|
let this = cx.entity().downgrade();
|
||||||
gpui::ListAlignment::Bottom,
|
move |index: usize, window, cx| {
|
||||||
px(2048.0),
|
let Some(this) = this.upgrade() else {
|
||||||
cx.processor({
|
return Empty.into_any();
|
||||||
move |this: &mut Self, index: usize, window, cx| {
|
};
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
let Some((entry, len)) = this.thread().and_then(|thread| {
|
let Some((entry, len)) = this.thread().and_then(|thread| {
|
||||||
let entries = &thread.read(cx).entries();
|
let entries = &thread.read(cx).entries();
|
||||||
Some((entries.get(index)?, entries.len()))
|
Some((entries.get(index)?, entries.len()))
|
||||||
|
@ -182,9 +183,9 @@ impl AcpThreadView {
|
||||||
return Empty.into_any();
|
return Empty.into_any();
|
||||||
};
|
};
|
||||||
this.render_entry(index, len, entry, window, cx)
|
this.render_entry(index, len, entry, window, cx)
|
||||||
}
|
})
|
||||||
}),
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
agent: agent.clone(),
|
agent: agent.clone(),
|
||||||
|
@ -2719,6 +2720,16 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_drop(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let (thread_view, _cx) = setup_thread_view(StubAgentServer::default(), cx).await;
|
||||||
|
let weak_view = thread_view.downgrade();
|
||||||
|
drop(thread_view);
|
||||||
|
assert!(!weak_view.is_upgradable());
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
|
async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
|
@ -970,13 +970,7 @@ impl AgentPanel {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.set_active_view(
|
this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
|
||||||
ActiveView::ExternalAgentThread {
|
|
||||||
thread_view: thread_view.clone(),
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
@ -1477,6 +1471,7 @@ impl AgentPanel {
|
||||||
|
|
||||||
let current_is_special = current_is_history || current_is_config;
|
let current_is_special = current_is_history || current_is_config;
|
||||||
let new_is_special = new_is_history || new_is_config;
|
let new_is_special = new_is_history || new_is_config;
|
||||||
|
let mut old_acp_thread = None;
|
||||||
|
|
||||||
match &self.active_view {
|
match &self.active_view {
|
||||||
ActiveView::Thread { thread, .. } => {
|
ActiveView::Thread { thread, .. } => {
|
||||||
|
@ -1488,6 +1483,9 @@ impl AgentPanel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ActiveView::ExternalAgentThread { thread_view } => {
|
||||||
|
old_acp_thread.replace(thread_view.downgrade());
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1518,6 +1516,11 @@ impl AgentPanel {
|
||||||
self.active_view = new_view;
|
self.active_view = new_view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug_assert!(
|
||||||
|
old_acp_thread.map_or(true, |thread| !thread.is_upgradable()),
|
||||||
|
"AcpThreadView leaked"
|
||||||
|
);
|
||||||
|
|
||||||
self.acp_message_history.borrow_mut().reset_position();
|
self.acp_message_history.borrow_mut().reset_position();
|
||||||
|
|
||||||
self.focus_handle(cx).focus(window);
|
self.focus_handle(cx).focus(window);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue