diff options
Diffstat (limited to 'packages/desktop/src-tauri/src/cli.rs')
| -rw-r--r-- | packages/desktop/src-tauri/src/cli.rs | 163 |
1 files changed, 117 insertions, 46 deletions
diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index b9e1ed4bd..dade1a281 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -1,35 +1,46 @@ +use futures::{FutureExt, Stream, StreamExt, future}; use tauri::{AppHandle, Manager, path::BaseDirectory}; use tauri_plugin_shell::{ ShellExt, - process::{Command, CommandChild, CommandEvent, TerminatedPayload}, + process::{CommandChild, CommandEvent, TerminatedPayload}, }; use tauri_plugin_store::StoreExt; +use tauri_specta::Event; use tokio::sync::oneshot; +use tracing::Instrument; use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY}; const CLI_INSTALL_DIR: &str = ".opencode/bin"; const CLI_BINARY_NAME: &str = "opencode"; -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, Debug)] pub struct ServerConfig { pub hostname: Option<String>, pub port: Option<u32>, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, Debug)] pub struct Config { pub server: Option<ServerConfig>, } pub async fn get_config(app: &AppHandle) -> Option<Config> { - create_command(app, "debug config", &[]) - .output() + let (events, _) = spawn_command(app, "debug config", &[]).ok()?; + + events + .fold(String::new(), async |mut config_str, event| { + if let CommandEvent::Stdout(stdout) = event + && let Ok(s) = str::from_utf8(&stdout) + { + config_str += s + } + + config_str + }) + .map(|v| serde_json::from_str::<Config>(&v)) .await - .inspect_err(|e| tracing::warn!("Failed to read OC config: {e}")) .ok() - .and_then(|out| String::from_utf8(out.stdout.to_vec()).ok()) - .and_then(|s| serde_json::from_str::<Config>(&s).ok()) } fn get_cli_install_path() -> Option<std::path::PathBuf> { @@ -175,7 +186,11 @@ fn shell_escape(input: &str) -> String { escaped } -pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, String)]) -> Command { +pub fn spawn_command( + app: &tauri::AppHandle, + args: &str, + extra_env: &[(&str, String)], +) -> Result<(impl Stream<Item = CommandEvent> + 'static, CommandChild), tauri_plugin_shell::Error> { let state_dir = app .path() .resolve("", BaseDirectory::AppLocalData) @@ -202,7 +217,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St .map(|(key, value)| (key.to_string(), value.clone())), ); - if cfg!(windows) { + let cmd = if cfg!(windows) { if is_wsl_enabled(app) { tracing::info!("WSL is enabled, spawning CLI server in WSL"); let version = app.package_info().version.to_string(); @@ -234,10 +249,9 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St script.push(format!("{} exec \"$BIN\" {}", env_prefix.join(" "), args)); - return app - .shell() + app.shell() .command("wsl") - .args(["-e", "bash", "-lc", &script.join("\n")]); + .args(["-e", "bash", "-lc", &script.join("\n")]) } else { let mut cmd = app .shell() @@ -249,7 +263,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St cmd = cmd.env(key, value); } - return cmd; + cmd } } else { let sidecar = get_sidecar_path(app); @@ -268,7 +282,13 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St } cmd - } + }; + + let (rx, child) = cmd.spawn()?; + let event_stream = tokio_stream::wrappers::ReceiverStream::new(rx); + let event_stream = sqlite_migration::logs_middleware(app.clone(), event_stream); + + Ok((event_stream, child)) } pub fn serve( @@ -286,45 +306,96 @@ pub fn serve( ("OPENCODE_SERVER_PASSWORD", password.to_string()), ]; - let (mut rx, child) = create_command( + let (events, child) = spawn_command( app, format!("--print-logs --log-level WARN serve --hostname {hostname} --port {port}").as_str(), &envs, ) - .spawn() .expect("Failed to spawn opencode"); - tokio::spawn(async move { - let mut exit_tx = Some(exit_tx); - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Stdout(line_bytes) => { - let line = String::from_utf8_lossy(&line_bytes); - tracing::info!(target: "sidecar", "{line}"); - } - CommandEvent::Stderr(line_bytes) => { - let line = String::from_utf8_lossy(&line_bytes); - tracing::info!(target: "sidecar", "{line}"); - } - CommandEvent::Error(err) => { - tracing::error!(target: "sidecar", "{err}"); - } - CommandEvent::Terminated(payload) => { - tracing::info!( - target: "sidecar", - code = ?payload.code, - signal = ?payload.signal, - "Sidecar terminated" - ); - - if let Some(tx) = exit_tx.take() { - let _ = tx.send(payload); + let mut exit_tx = Some(exit_tx); + tokio::spawn( + events + .for_each(move |event| { + match event { + CommandEvent::Stdout(line_bytes) => { + let line = String::from_utf8_lossy(&line_bytes); + tracing::info!("{line}"); + } + CommandEvent::Stderr(line_bytes) => { + let line = String::from_utf8_lossy(&line_bytes); + tracing::info!("{line}"); } + CommandEvent::Error(err) => { + tracing::error!("{err}"); + } + CommandEvent::Terminated(payload) => { + tracing::info!( + code = ?payload.code, + signal = ?payload.signal, + "Sidecar terminated" + ); + + if let Some(tx) = exit_tx.take() { + let _ = tx.send(payload); + } + } + _ => {} } - _ => {} - } - } - }); + + future::ready(()) + }) + .instrument(tracing::info_span!("sidecar")), + ); (child, exit_rx) } + +pub mod sqlite_migration { + use super::*; + + #[derive( + tauri_specta::Event, serde::Serialize, serde::Deserialize, Clone, Copy, Debug, specta::Type, + )] + #[serde(tag = "type", content = "value")] + pub enum SqliteMigrationProgress { + InProgress(u8), + Done, + } + + pub(super) fn logs_middleware( + app: AppHandle, + stream: impl Stream<Item = CommandEvent>, + ) -> impl Stream<Item = CommandEvent> { + let app = app.clone(); + let mut done = false; + + stream.filter_map(move |event| { + if done { + return future::ready(Some(event)); + } + + future::ready(match &event { + CommandEvent::Stdout(stdout) => { + let Ok(s) = str::from_utf8(stdout) else { + return future::ready(None); + }; + + if let Some(s) = s.strip_prefix("sqlite-migration:").map(|s| s.trim()) { + if let Ok(progress) = s.parse::<u8>() { + let _ = SqliteMigrationProgress::InProgress(progress).emit(&app); + } else if s == "done" { + done = true; + let _ = SqliteMigrationProgress::Done.emit(&app); + } + + None + } else { + Some(event) + } + } + _ => Some(event), + }) + }) + } +} |
