|
@@ -1,35 +1,46 @@
|
|
|
|
|
+use futures::{FutureExt, Stream, StreamExt, future};
|
|
|
use tauri::{AppHandle, Manager, path::BaseDirectory};
|
|
use tauri::{AppHandle, Manager, path::BaseDirectory};
|
|
|
use tauri_plugin_shell::{
|
|
use tauri_plugin_shell::{
|
|
|
ShellExt,
|
|
ShellExt,
|
|
|
- process::{Command, CommandChild, CommandEvent, TerminatedPayload},
|
|
|
|
|
|
|
+ process::{CommandChild, CommandEvent, TerminatedPayload},
|
|
|
};
|
|
};
|
|
|
use tauri_plugin_store::StoreExt;
|
|
use tauri_plugin_store::StoreExt;
|
|
|
|
|
+use tauri_specta::Event;
|
|
|
use tokio::sync::oneshot;
|
|
use tokio::sync::oneshot;
|
|
|
|
|
+use tracing::Instrument;
|
|
|
|
|
|
|
|
use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY};
|
|
use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY};
|
|
|
|
|
|
|
|
const CLI_INSTALL_DIR: &str = ".opencode/bin";
|
|
const CLI_INSTALL_DIR: &str = ".opencode/bin";
|
|
|
const CLI_BINARY_NAME: &str = "opencode";
|
|
const CLI_BINARY_NAME: &str = "opencode";
|
|
|
|
|
|
|
|
-#[derive(serde::Deserialize)]
|
|
|
|
|
|
|
+#[derive(serde::Deserialize, Debug)]
|
|
|
pub struct ServerConfig {
|
|
pub struct ServerConfig {
|
|
|
pub hostname: Option<String>,
|
|
pub hostname: Option<String>,
|
|
|
pub port: Option<u32>,
|
|
pub port: Option<u32>,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-#[derive(serde::Deserialize)]
|
|
|
|
|
|
|
+#[derive(serde::Deserialize, Debug)]
|
|
|
pub struct Config {
|
|
pub struct Config {
|
|
|
pub server: Option<ServerConfig>,
|
|
pub server: Option<ServerConfig>,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_config(app: &AppHandle) -> Option<Config> {
|
|
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
|
|
.await
|
|
|
- .inspect_err(|e| tracing::warn!("Failed to read OC config: {e}"))
|
|
|
|
|
.ok()
|
|
.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> {
|
|
fn get_cli_install_path() -> Option<std::path::PathBuf> {
|
|
@@ -175,7 +186,11 @@ fn shell_escape(input: &str) -> String {
|
|
|
escaped
|
|
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
|
|
let state_dir = app
|
|
|
.path()
|
|
.path()
|
|
|
.resolve("", BaseDirectory::AppLocalData)
|
|
.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())),
|
|
.map(|(key, value)| (key.to_string(), value.clone())),
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- if cfg!(windows) {
|
|
|
|
|
|
|
+ let cmd = if cfg!(windows) {
|
|
|
if is_wsl_enabled(app) {
|
|
if is_wsl_enabled(app) {
|
|
|
tracing::info!("WSL is enabled, spawning CLI server in WSL");
|
|
tracing::info!("WSL is enabled, spawning CLI server in WSL");
|
|
|
let version = app.package_info().version.to_string();
|
|
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));
|
|
script.push(format!("{} exec \"$BIN\" {}", env_prefix.join(" "), args));
|
|
|
|
|
|
|
|
- return app
|
|
|
|
|
- .shell()
|
|
|
|
|
|
|
+ app.shell()
|
|
|
.command("wsl")
|
|
.command("wsl")
|
|
|
- .args(["-e", "bash", "-lc", &script.join("\n")]);
|
|
|
|
|
|
|
+ .args(["-e", "bash", "-lc", &script.join("\n")])
|
|
|
} else {
|
|
} else {
|
|
|
let mut cmd = app
|
|
let mut cmd = app
|
|
|
.shell()
|
|
.shell()
|
|
@@ -249,7 +263,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
|
|
|
cmd = cmd.env(key, value);
|
|
cmd = cmd.env(key, value);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return cmd;
|
|
|
|
|
|
|
+ cmd
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
let sidecar = get_sidecar_path(app);
|
|
let sidecar = get_sidecar_path(app);
|
|
@@ -268,7 +282,13 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
cmd
|
|
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(
|
|
pub fn serve(
|
|
@@ -286,45 +306,96 @@ pub fn serve(
|
|
|
("OPENCODE_SERVER_PASSWORD", password.to_string()),
|
|
("OPENCODE_SERVER_PASSWORD", password.to_string()),
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
- let (mut rx, child) = create_command(
|
|
|
|
|
|
|
+ let (events, child) = spawn_command(
|
|
|
app,
|
|
app,
|
|
|
format!("--print-logs --log-level WARN serve --hostname {hostname} --port {port}").as_str(),
|
|
format!("--print-logs --log-level WARN serve --hostname {hostname} --port {port}").as_str(),
|
|
|
&envs,
|
|
&envs,
|
|
|
)
|
|
)
|
|
|
- .spawn()
|
|
|
|
|
.expect("Failed to spawn opencode");
|
|
.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)
|
|
(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),
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|