1+ use futures:: { FutureExt , Stream , StreamExt , future} ;
12use tauri:: { AppHandle , Manager , path:: BaseDirectory } ;
23use tauri_plugin_shell:: {
34 ShellExt ,
4- process:: { Command , CommandChild , CommandEvent , TerminatedPayload } ,
5+ process:: { CommandChild , CommandEvent , TerminatedPayload } ,
56} ;
67use tauri_plugin_store:: StoreExt ;
8+ use tauri_specta:: Event ;
79use tokio:: sync:: oneshot;
10+ use tracing:: Instrument ;
811
912use crate :: constants:: { SETTINGS_STORE , WSL_ENABLED_KEY } ;
1013
1114const CLI_INSTALL_DIR : & str = ".opencode/bin" ;
1215const CLI_BINARY_NAME : & str = "opencode" ;
1316
14- #[ derive( serde:: Deserialize ) ]
17+ #[ derive( serde:: Deserialize , Debug ) ]
1518pub struct ServerConfig {
1619 pub hostname : Option < String > ,
1720 pub port : Option < u32 > ,
1821}
1922
20- #[ derive( serde:: Deserialize ) ]
23+ #[ derive( serde:: Deserialize , Debug ) ]
2124pub struct Config {
2225 pub server : Option < ServerConfig > ,
2326}
2427
2528pub 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
3546fn 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
274294pub 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