Skip to content

Commit 89ded90

Browse files
Brendonovichschneiderlo
authored andcommitted
desktop: sqlite migration progress bar (anomalyco#13294)
1 parent c5fed68 commit 89ded90

7 files changed

Lines changed: 197 additions & 80 deletions

File tree

packages/desktop/src-tauri/Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ tracing = "0.1"
5151
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
5252
tracing-appender = "0.2"
5353
chrono = "0.4"
54+
tokio-stream = { version = "0.1.18", features = ["sync"] }
5455

5556
[target.'cfg(target_os = "linux")'.dependencies]
5657
gtk = "0.18.2"

packages/desktop/src-tauri/src/cli.rs

Lines changed: 117 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,46 @@
1+
use futures::{FutureExt, Stream, StreamExt, future};
12
use tauri::{AppHandle, Manager, path::BaseDirectory};
23
use tauri_plugin_shell::{
34
ShellExt,
4-
process::{Command, CommandChild, CommandEvent, TerminatedPayload},
5+
process::{CommandChild, CommandEvent, TerminatedPayload},
56
};
67
use tauri_plugin_store::StoreExt;
8+
use tauri_specta::Event;
79
use tokio::sync::oneshot;
10+
use tracing::Instrument;
811

912
use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY};
1013

1114
const CLI_INSTALL_DIR: &str = ".opencode/bin";
1215
const CLI_BINARY_NAME: &str = "opencode";
1316

14-
#[derive(serde::Deserialize)]
17+
#[derive(serde::Deserialize, Debug)]
1518
pub struct ServerConfig {
1619
pub hostname: Option<String>,
1720
pub port: Option<u32>,
1821
}
1922

20-
#[derive(serde::Deserialize)]
23+
#[derive(serde::Deserialize, Debug)]
2124
pub struct Config {
2225
pub server: Option<ServerConfig>,
2326
}
2427

2528
pub async fn get_config(app: &AppHandle) -> Option<Config> {
26-
create_command(app, "debug config", &[])
27-
.output()
29+
let (events, _) = spawn_command(app, "debug config", &[]).ok()?;
30+
31+
events
32+
.fold(String::new(), async |mut config_str, event| {
33+
if let CommandEvent::Stdout(stdout) = event
34+
&& let Ok(s) = str::from_utf8(&stdout)
35+
{
36+
config_str += s
37+
}
38+
39+
config_str
40+
})
41+
.map(|v| serde_json::from_str::<Config>(&v))
2842
.await
29-
.inspect_err(|e| tracing::warn!("Failed to read OC config: {e}"))
3043
.ok()
31-
.and_then(|out| String::from_utf8(out.stdout.to_vec()).ok())
32-
.and_then(|s| serde_json::from_str::<Config>(&s).ok())
3344
}
3445

3546
fn get_cli_install_path() -> Option<std::path::PathBuf> {
@@ -175,7 +186,11 @@ fn shell_escape(input: &str) -> String {
175186
escaped
176187
}
177188

178-
pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, String)]) -> Command {
189+
pub fn spawn_command(
190+
app: &tauri::AppHandle,
191+
args: &str,
192+
extra_env: &[(&str, String)],
193+
) -> Result<(impl Stream<Item = CommandEvent> + 'static, CommandChild), tauri_plugin_shell::Error> {
179194
let state_dir = app
180195
.path()
181196
.resolve("", BaseDirectory::AppLocalData)
@@ -202,7 +217,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
202217
.map(|(key, value)| (key.to_string(), value.clone())),
203218
);
204219

205-
if cfg!(windows) {
220+
let cmd = if cfg!(windows) {
206221
if is_wsl_enabled(app) {
207222
tracing::info!("WSL is enabled, spawning CLI server in WSL");
208223
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
234249

235250
script.push(format!("{} exec \"$BIN\" {}", env_prefix.join(" "), args));
236251

237-
return app
238-
.shell()
252+
app.shell()
239253
.command("wsl")
240-
.args(["-e", "bash", "-lc", &script.join("\n")]);
254+
.args(["-e", "bash", "-lc", &script.join("\n")])
241255
} else {
242256
let mut cmd = app
243257
.shell()
@@ -249,7 +263,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
249263
cmd = cmd.env(key, value);
250264
}
251265

252-
return cmd;
266+
cmd
253267
}
254268
} else {
255269
let sidecar = get_sidecar_path(app);
@@ -268,7 +282,13 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
268282
}
269283

270284
cmd
271-
}
285+
};
286+
287+
let (rx, child) = cmd.spawn()?;
288+
let event_stream = tokio_stream::wrappers::ReceiverStream::new(rx);
289+
let event_stream = sqlite_migration::logs_middleware(app.clone(), event_stream);
290+
291+
Ok((event_stream, child))
272292
}
273293

274294
pub fn serve(
@@ -286,45 +306,96 @@ pub fn serve(
286306
("OPENCODE_SERVER_PASSWORD", password.to_string()),
287307
];
288308

289-
let (mut rx, child) = create_command(
309+
let (events, child) = spawn_command(
290310
app,
291311
format!("--print-logs --log-level WARN serve --hostname {hostname} --port {port}").as_str(),
292312
&envs,
293313
)
294-
.spawn()
295314
.expect("Failed to spawn opencode");
296315

297-
tokio::spawn(async move {
298-
let mut exit_tx = Some(exit_tx);
299-
while let Some(event) = rx.recv().await {
300-
match event {
301-
CommandEvent::Stdout(line_bytes) => {
302-
let line = String::from_utf8_lossy(&line_bytes);
303-
tracing::info!(target: "sidecar", "{line}");
304-
}
305-
CommandEvent::Stderr(line_bytes) => {
306-
let line = String::from_utf8_lossy(&line_bytes);
307-
tracing::info!(target: "sidecar", "{line}");
308-
}
309-
CommandEvent::Error(err) => {
310-
tracing::error!(target: "sidecar", "{err}");
311-
}
312-
CommandEvent::Terminated(payload) => {
313-
tracing::info!(
314-
target: "sidecar",
315-
code = ?payload.code,
316-
signal = ?payload.signal,
317-
"Sidecar terminated"
318-
);
319-
320-
if let Some(tx) = exit_tx.take() {
321-
let _ = tx.send(payload);
316+
let mut exit_tx = Some(exit_tx);
317+
tokio::spawn(
318+
events
319+
.for_each(move |event| {
320+
match event {
321+
CommandEvent::Stdout(line_bytes) => {
322+
let line = String::from_utf8_lossy(&line_bytes);
323+
tracing::info!("{line}");
324+
}
325+
CommandEvent::Stderr(line_bytes) => {
326+
let line = String::from_utf8_lossy(&line_bytes);
327+
tracing::info!("{line}");
322328
}
329+
CommandEvent::Error(err) => {
330+
tracing::error!("{err}");
331+
}
332+
CommandEvent::Terminated(payload) => {
333+
tracing::info!(
334+
code = ?payload.code,
335+
signal = ?payload.signal,
336+
"Sidecar terminated"
337+
);
338+
339+
if let Some(tx) = exit_tx.take() {
340+
let _ = tx.send(payload);
341+
}
342+
}
343+
_ => {}
323344
}
324-
_ => {}
325-
}
326-
}
327-
});
345+
346+
future::ready(())
347+
})
348+
.instrument(tracing::info_span!("sidecar")),
349+
);
328350

329351
(child, exit_rx)
330352
}
353+
354+
pub mod sqlite_migration {
355+
use super::*;
356+
357+
#[derive(
358+
tauri_specta::Event, serde::Serialize, serde::Deserialize, Clone, Copy, Debug, specta::Type,
359+
)]
360+
#[serde(tag = "type", content = "value")]
361+
pub enum SqliteMigrationProgress {
362+
InProgress(u8),
363+
Done,
364+
}
365+
366+
pub(super) fn logs_middleware(
367+
app: AppHandle,
368+
stream: impl Stream<Item = CommandEvent>,
369+
) -> impl Stream<Item = CommandEvent> {
370+
let app = app.clone();
371+
let mut done = false;
372+
373+
stream.filter_map(move |event| {
374+
if done {
375+
return future::ready(Some(event));
376+
}
377+
378+
future::ready(match &event {
379+
CommandEvent::Stdout(stdout) => {
380+
let Ok(s) = str::from_utf8(stdout) else {
381+
return future::ready(None);
382+
};
383+
384+
if let Some(s) = s.strip_prefix("sqlite-migration:").map(|s| s.trim()) {
385+
if let Ok(progress) = s.parse::<u8>() {
386+
let _ = SqliteMigrationProgress::InProgress(progress).emit(&app);
387+
} else if s == "done" {
388+
done = true;
389+
let _ = SqliteMigrationProgress::Done.emit(&app);
390+
}
391+
392+
None
393+
} else {
394+
Some(event)
395+
}
396+
}
397+
_ => Some(event),
398+
})
399+
})
400+
}
401+
}

0 commit comments

Comments
 (0)