chore: package the goose binary in the goose2 tauri app (#8615)

Co-authored-by: Lifei Zhou <lifei@squareup.com>
This commit is contained in:
Jack Amadeo
2026-04-17 03:01:30 -04:00
committed by GitHub
parent bd14186214
commit 75a41a34dc
21 changed files with 857 additions and 519 deletions
+2 -3
View File
@@ -1,8 +1,7 @@
use crate::services::acp::GooseServeProcess;
#[tauri::command]
pub async fn get_goose_serve_url() -> Result<String, String> {
GooseServeProcess::start().await?;
let process = GooseServeProcess::get()?;
pub async fn get_goose_serve_url(app_handle: tauri::AppHandle) -> Result<String, String> {
let process = GooseServeProcess::get(app_handle).await?;
Ok(process.ws_url())
}
@@ -1,7 +1,7 @@
use tauri::{AppHandle, Emitter};
use tokio::io::{AsyncBufReadExt, BufReader};
use crate::services::acp::resolve_goose_binary;
use crate::services::acp::goose_serve::get_goose_command;
#[derive(serde::Serialize, Clone)]
#[serde(rename_all = "camelCase")]
@@ -20,9 +20,9 @@ pub async fn authenticate_model_provider(
return Err("Native Goose sign-in is not supported on Windows yet".to_string());
}
let goose_binary = resolve_goose_binary()?;
let goose_command = get_goose_command(&app_handle)?;
let quoted_label = shell_quote(&provider_label);
let quoted_binary = shell_quote(&goose_binary.to_string_lossy());
let quoted_binary = shell_quote(&goose_command.as_std().get_program().to_string_lossy());
let command = if cfg!(target_os = "linux") {
format!(
+1
View File
@@ -9,6 +9,7 @@ use tauri_plugin_window_state::StateFlags;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(
tauri_plugin_log::Builder::new()
.level(log::LevelFilter::Debug)
@@ -1,4 +1,6 @@
use std::path::{Path, PathBuf};
use tauri_plugin_shell::ShellExt;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use tokio::process::{Child, Command};
@@ -7,12 +9,6 @@ use tokio::sync::OnceCell;
const GOOSE_SERVE_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
const GOOSE_SERVE_CONNECT_RETRY_DELAY: Duration = Duration::from_millis(100);
const LOCALHOST: &str = "127.0.0.1";
const COMMON_GOOSE_PATHS: &[&str] = &[
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/home/linuxbrew/.linuxbrew/bin",
];
// ---------------------------------------------------------------------------
// GooseServeProcess — singleton that owns the long-lived `goose serve` child
// ---------------------------------------------------------------------------
@@ -36,29 +32,15 @@ impl GooseServeProcess {
format!("ws://{LOCALHOST}:{}/acp", self.port)
}
/// Start the singleton `goose serve` process.
///
/// This is called once from `lib.rs` during app startup. Subsequent calls
/// are no-ops (the `OnceCell` ensures single initialisation). The process
/// is spawned with `kill_on_drop(true)` so it is automatically terminated
/// when the Tauri app exits.
pub async fn start() -> Result<(), String> {
GOOSE_SERVE
.get_or_try_init(|| async { Self::spawn().await })
.await
.map(|_| ())
}
/// Get a reference to the running process, or an error if it was never
/// started (should not happen in normal operation).
pub fn get() -> Result<&'static GooseServeProcess, String> {
pub async fn get(app_handle: tauri::AppHandle) -> Result<&'static GooseServeProcess, String> {
GOOSE_SERVE
.get()
.ok_or_else(|| "Goose serve process has not been started".to_string())
.get_or_try_init(|| async { Self::spawn(app_handle).await })
.await
}
async fn spawn() -> Result<GooseServeProcess, String> {
let binary_path = resolve_goose_binary()?;
async fn spawn(app_handle: tauri::AppHandle) -> Result<GooseServeProcess, String> {
let port = reserve_free_port()?;
// Use a stable working directory for the long-lived server process.
@@ -71,7 +53,9 @@ impl GooseServeProcess {
)
})?;
let mut command = Command::new(&binary_path);
let mut command: Command = get_goose_command(&app_handle)?;
let binary_display = command.as_std().get_program().to_string_lossy().to_string();
command
.arg("serve")
.arg("--host")
@@ -85,16 +69,13 @@ impl GooseServeProcess {
.kill_on_drop(true);
log::info!(
"Spawning long-lived goose serve: binary={} port={} cwd={}",
binary_path.display(),
port,
"Spawning long-lived goose serve: binary={binary_display} port={port} cwd={}",
working_dir.display(),
);
let mut child = command.spawn().map_err(|error| {
format!(
"Failed to spawn goose serve (binary: {}, cwd: {}): {error}",
binary_path.display(),
"Failed to spawn goose serve (binary: {binary_display}, cwd: {}): {error}",
working_dir.display()
)
})?;
@@ -110,6 +91,19 @@ impl GooseServeProcess {
}
}
pub fn get_goose_command(app_handle: &tauri::AppHandle) -> Result<Command, String> {
if let Ok(override_path) = std::env::var("GOOSE_BIN") {
Ok(Command::new(override_path))
} else {
let tauri_command = app_handle
.shell()
.sidecar("goose")
.map_err(|e| format!("could not resolve goose binary: {e}"))?;
let std_command: std::process::Command = tauri_command.into();
Ok(std_command.into())
}
}
async fn wait_for_server_ready(port: u16, child: &mut Child) -> Result<(), String> {
let deadline = Instant::now() + GOOSE_SERVE_CONNECT_TIMEOUT;
let addr = format!("{LOCALHOST}:{port}");
@@ -144,164 +138,6 @@ fn default_serve_working_dir() -> PathBuf {
.join("artifacts")
}
// ---------------------------------------------------------------------------
// Binary resolution
// ---------------------------------------------------------------------------
pub(crate) fn resolve_goose_binary() -> Result<PathBuf, String> {
let binary_path = if let Ok(override_path) = std::env::var("GOOSE_BIN") {
let path = PathBuf::from(&override_path);
if !path.exists() {
return Err(format!(
"GOOSE_BIN points to non-existent path: {override_path}"
));
}
if !goose_binary_supports_serve(&path)? {
return Err(format!(
"GOOSE_BIN points to a goose binary without `serve` support: {}",
path.display()
));
}
log::info!("Using GOOSE_BIN override: {override_path}");
path
} else {
let path = find_goose_binary()
.ok_or_else(|| "Unknown or unavailable agent provider: goose".to_string())?;
if !goose_binary_supports_serve(&path)? {
return Err(format!(
"Resolved goose binary does not support `serve`: {}. Set GOOSE_BIN to a newer goose binary.",
path.display()
));
}
log::info!(
"Resolved goose binary via local discovery: {}",
path.display()
);
path
};
// Log the binary version for debugging.
match std::process::Command::new(&binary_path)
.env("GOOSE_PATH_ROOT", goose_probe_root())
.arg("--version")
.output()
{
Ok(output) => {
let version = String::from_utf8_lossy(&output.stdout);
log::info!(
"Goose binary version: {} (path: {})",
version.trim(),
binary_path.display()
);
}
Err(err) => {
log::warn!(
"Could not determine goose binary version at {}: {err}",
binary_path.display()
);
}
}
Ok(binary_path)
}
fn find_goose_binary() -> Option<PathBuf> {
find_goose_via_login_shell().or_else(find_goose_in_common_paths)
}
fn find_goose_via_login_shell() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
None
}
#[cfg(not(target_os = "windows"))]
{
for shell in ["/bin/zsh", "/bin/bash"] {
let Ok(output) = std::process::Command::new(shell)
.args(["-l", "-c", "which goose"])
.output()
else {
continue;
};
if !output.status.success() {
continue;
}
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(path_str) = stdout.lines().rfind(|line| !line.trim().is_empty()) {
let path = PathBuf::from(path_str.trim());
if path.is_file() {
return Some(path);
}
}
}
None
}
}
fn find_goose_in_common_paths() -> Option<PathBuf> {
COMMON_GOOSE_PATHS
.iter()
.map(|dir| Path::new(dir).join(goose_binary_name()))
.find(|path| path.is_file())
}
fn goose_binary_name() -> &'static str {
#[cfg(target_os = "windows")]
{
"goose.exe"
}
#[cfg(not(target_os = "windows"))]
{
"goose"
}
}
fn goose_binary_supports_serve(binary_path: &PathBuf) -> Result<bool, String> {
let output = std::process::Command::new(binary_path)
.env("GOOSE_PATH_ROOT", goose_probe_root())
.arg("serve")
.arg("--help")
.output()
.map_err(|error| {
format!(
"Failed to probe goose binary {}: {error}",
binary_path.display()
)
})?;
if output.status.success() {
return Ok(true);
}
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
log::warn!(
"Goose binary probe failed for {}: status={} stderr={} stdout={}",
binary_path.display(),
output
.status
.code()
.map(|code| code.to_string())
.unwrap_or_else(|| "signal".to_string()),
stderr.trim(),
stdout.trim(),
);
Ok(false)
}
fn goose_probe_root() -> PathBuf {
std::env::temp_dir().join("block-goose2-goose-probe")
}
fn reserve_free_port() -> Result<u16, String> {
let listener = std::net::TcpListener::bind((LOCALHOST, 0))
.map_err(|error| format!("Failed to reserve Goose serve port: {error}"))?;
@@ -1,4 +1,3 @@
pub(crate) mod goose_serve;
pub(crate) use goose_serve::resolve_goose_binary;
pub(crate) use goose_serve::GooseServeProcess;