Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/commands/up/harness_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ async fn managed_setup(
follow_up: &mut FollowUp,
opencode_host: &crate::local::opencode::AgentHost,
) -> Result<Option<CommandFailure>, String> {
let _setup = managed::lock_installations().map_err(|error| error.to_string())?;
let _setup = wait_for_installations(socket).await?;
let harness = request.harness.as_str();
let user = managed::user_install(harness).await;
let copy = managed::installed(harness);
Expand Down Expand Up @@ -707,6 +707,33 @@ async fn install_with_progress(
}
}

/// The installations lock, once another install, removal or background update lets go of it.
async fn wait_for_installations(
socket: &mut WebSocket,
) -> Result<crate::file_lock::FileLock, String> {
let error = |error: crate::error::Error| error.to_string();
if let Some(lock) = managed::try_lock_installations().map_err(error)? {
return Ok(lock);
}
write_terminal(socket, "Waiting for another agent install to finish…\r\n")
.await
.map_err(|error| error.to_string())?;
loop {
tokio::select! {
_ = tokio::time::sleep(crate::config::paced(std::time::Duration::from_millis(250))) => {
if let Some(lock) = managed::try_lock_installations().map_err(error)? {
return Ok(lock);
}
}
message = socket.recv() => {
if matches!(message, None | Some(Err(_)) | Some(Ok(Message::Close(_)))) {
return Err("The install was cancelled.".into());
}
}
}
}
}

/// The same progress as data, for a client that draws a progress bar in place of the terminal.
async fn write_progress(
socket: &mut WebSocket,
Expand Down
35 changes: 28 additions & 7 deletions src/local/harness/managed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,29 @@ fn root() -> PathBuf {

/// Hold across install decisions and removal so dashboards cannot race shared runtime cleanup.
pub(crate) fn lock_installations() -> Result<crate::file_lock::FileLock> {
try_lock_installations()?.ok_or_else(|| {
anyhow!("Another agent install or removal is in progress. Try again when it finishes.")
})
}

/// [`lock_installations`], or `None` while another install or removal holds it.
pub(crate) fn try_lock_installations() -> Result<Option<crate::file_lock::FileLock>> {
let root = crate::config::config_dir();
std::fs::create_dir_all(&root)?;
let path = root.join("managed-agents.lock");
let lock = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(root.join("managed-agents.lock"))?;
lock.try_lock().map_err(|error| {
anyhow!(
"Another agent install or removal is in progress. Try again when it finishes: {error}"
)
})?;
Ok(crate::file_lock::FileLock::new(lock))
.open(&path)?;
match lock.try_lock() {
Ok(()) => Ok(Some(crate::file_lock::FileLock::new(lock))),
Err(std::fs::TryLockError::WouldBlock) => Ok(None),
Err(std::fs::TryLockError::Error(error)) => {
Err(anyhow!("Couldn't lock {}: {error}", path.display()))
}
}
}

fn executable(harness: &str, release: &Path) -> PathBuf {
Expand Down Expand Up @@ -819,6 +828,18 @@ mod tests {
}
}

#[test]
fn an_install_in_progress_reads_as_busy_rather_than_failing() {
let _held = lock_installations().unwrap();
assert!(try_lock_installations().unwrap().is_none());
let Err(busy) = lock_installations() else {
panic!("a second install took the lock");
};
assert!(busy
.to_string()
.starts_with("Another agent install or removal is in progress"));
}

#[tokio::test]
async fn uninstall_validates_before_touching_any_path() {
let directory =
Expand Down
5 changes: 5 additions & 0 deletions src/local/harness/opencode_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,11 @@ async fn discover_models_within(
post(endpoint, "/api/plugin/await-activation", &json!({})).await?;
}
loop {
if !endpoint.legacy_v2_api && !endpoint.plugins_active().await? {
plugins_pending = true;
tokio::time::sleep(retry).await;
continue;
}
let models = catalog_get(endpoint, "/api/model").await?;
plugins_pending = models.is_none();
if let Some(models) = models {
Expand Down
Loading