desktop: sqlite migration progress bar (#13294)

This commit is contained in:
Brendan Allan
2026-02-12 17:44:06 +08:00
committed by GitHub
parent 624dd94b5d
commit 1413d77b1f
7 changed files with 197 additions and 80 deletions

View File

@@ -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),
})
})
}
}