Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"
chrono = "0.4"
tokio-stream = { version = "0.1.18", features = ["sync"] }

[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18.2"
Expand Down
163 changes: 117 additions & 46 deletions packages/desktop/src-tauri/src/cli.rs
Original file line number Diff line number Diff line change
@@ -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> {
Expand Down Expand Up @@ -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)
Expand All @@ -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();
Expand Down Expand Up @@ -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()
Expand All @@ -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);
Expand All @@ -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(
Expand All @@ -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}");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should stderr lines be logged as warnings?

Suggested change
tracing::info!("{line}");
tracing::warn!("{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),
})
})
}
}
Loading
Loading