lib.rs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. use std::{
  2. net::{SocketAddr, TcpListener},
  3. process::Command,
  4. sync::{Arc, Mutex},
  5. time::{Duration, Instant},
  6. };
  7. #[cfg(target_os = "macos")]
  8. use tauri::TitleBarStyle;
  9. use tauri::{AppHandle, LogicalSize, Manager, Monitor, RunEvent, WebviewUrl, WebviewWindow};
  10. use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
  11. use tauri_plugin_shell::process::{CommandChild, CommandEvent};
  12. use tauri_plugin_shell::ShellExt;
  13. use tokio::net::TcpSocket;
  14. #[derive(Clone)]
  15. struct ServerState(Arc<Mutex<Option<CommandChild>>>);
  16. fn get_sidecar_port() -> u16 {
  17. option_env!("OPENCODE_PORT")
  18. .map(|s| s.to_string())
  19. .or_else(|| std::env::var("OPENCODE_PORT").ok())
  20. .and_then(|port_str| port_str.parse().ok())
  21. .unwrap_or_else(|| {
  22. TcpListener::bind("127.0.0.1:0")
  23. .expect("Failed to bind to find free port")
  24. .local_addr()
  25. .expect("Failed to get local address")
  26. .port()
  27. })
  28. }
  29. fn find_and_kill_process_on_port(port: u16) -> Result<(), Box<dyn std::error::Error>> {
  30. // Find all listeners on the specified port
  31. let listeners = listeners::get_processes_by_port(port)?;
  32. if listeners.is_empty() {
  33. println!("No processes found listening on port {}", port);
  34. return Ok(());
  35. }
  36. for listener in listeners {
  37. let pid = listener.pid;
  38. println!("Found process {} listening on port {}", pid, port);
  39. // Kill the process using platform-appropriate command
  40. #[cfg(target_os = "windows")]
  41. {
  42. Command::new("taskkill")
  43. .args(["/F", "/PID", &pid.to_string()])
  44. .output()?;
  45. }
  46. #[cfg(not(target_os = "windows"))]
  47. {
  48. Command::new("kill")
  49. .args(["-9", &pid.to_string()])
  50. .output()?;
  51. }
  52. println!("Killed process {}", pid);
  53. }
  54. Ok(())
  55. }
  56. fn spawn_sidecar(app: &AppHandle, port: u16) -> CommandChild {
  57. let (mut rx, child) = app
  58. .shell()
  59. .sidecar("opencode-cli")
  60. .unwrap()
  61. .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
  62. .env("OPENCODE_CLIENT", "desktop")
  63. .args(["serve", &format!("--port={port}")])
  64. .spawn()
  65. .expect("Failed to spawn opencode");
  66. tauri::async_runtime::spawn(async move {
  67. while let Some(event) = rx.recv().await {
  68. match event {
  69. CommandEvent::Stdout(line_bytes) => {
  70. let line = String::from_utf8_lossy(&line_bytes);
  71. print!("{line}");
  72. }
  73. CommandEvent::Stderr(line_bytes) => {
  74. let line = String::from_utf8_lossy(&line_bytes);
  75. eprint!("{line}");
  76. }
  77. _ => {}
  78. }
  79. }
  80. });
  81. child
  82. }
  83. async fn is_server_running(port: u16) -> bool {
  84. TcpSocket::new_v4()
  85. .unwrap()
  86. .connect(SocketAddr::new(
  87. "127.0.0.1".parse().expect("Failed to parse IP"),
  88. port,
  89. ))
  90. .await
  91. .is_ok()
  92. }
  93. #[cfg_attr(mobile, tauri::mobile_entry_point)]
  94. pub fn run() {
  95. let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
  96. let mut builder = tauri::Builder::default()
  97. .plugin(tauri_plugin_os::init())
  98. .plugin(tauri_plugin_window_state::Builder::new().build())
  99. .plugin(tauri_plugin_store::Builder::new().build())
  100. .plugin(tauri_plugin_dialog::init())
  101. .plugin(tauri_plugin_shell::init())
  102. .plugin(tauri_plugin_process::init())
  103. .plugin(tauri_plugin_opener::init())
  104. .setup(move |app| {
  105. let app = app.handle().clone();
  106. tauri::async_runtime::spawn(async move {
  107. let port = get_sidecar_port();
  108. let should_spawn_sidecar = !is_server_running(port).await;
  109. // if server_running {
  110. // let res = app
  111. // .dialog()
  112. // .message(
  113. // "OpenCode Server is already running, would you like to restart it?",
  114. // )
  115. // .buttons(MessageDialogButtons::YesNo)
  116. // .blocking_show_with_result();
  117. // match res {
  118. // MessageDialogResult::Yes => {
  119. // if let Err(e) = find_and_kill_process_on_port(port) {
  120. // eprintln!("Failed to kill process on port {}: {}", port, e);
  121. // }
  122. // true
  123. // }
  124. // _ => false,
  125. // }
  126. // } else {
  127. // true
  128. // };
  129. let child = if should_spawn_sidecar {
  130. let child = spawn_sidecar(&app, port);
  131. let timestamp = Instant::now();
  132. loop {
  133. if timestamp.elapsed() > Duration::from_secs(7) {
  134. todo!("Handle server spawn timeout");
  135. }
  136. tokio::time::sleep(Duration::from_millis(10)).await;
  137. if is_server_running(port).await {
  138. // give the server a little bit more time to warm up
  139. tokio::time::sleep(Duration::from_millis(10)).await;
  140. break;
  141. }
  142. }
  143. println!("Server ready after {:?}", timestamp.elapsed());
  144. Some(child)
  145. } else {
  146. None
  147. };
  148. let primary_monitor = app.primary_monitor().ok().flatten();
  149. let size = primary_monitor
  150. .map(|m| m.size().to_logical(m.scale_factor()))
  151. .unwrap_or(LogicalSize::new(1920, 1080));
  152. let mut window_builder =
  153. WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
  154. .title("OpenCode")
  155. .inner_size(size.width as f64, size.height as f64)
  156. .decorations(true)
  157. .zoom_hotkeys_enabled(true)
  158. .disable_drag_drop_handler()
  159. .initialization_script(format!(
  160. r#"
  161. window.__OPENCODE__ ??= {{}};
  162. window.__OPENCODE__.updaterEnabled = {updater_enabled};
  163. window.__OPENCODE__.port = {port};
  164. "#
  165. ));
  166. #[cfg(target_os = "macos")]
  167. {
  168. window_builder = window_builder
  169. .title_bar_style(TitleBarStyle::Overlay)
  170. .hidden_title(true);
  171. }
  172. window_builder.build().expect("Failed to create window");
  173. app.manage(ServerState(Arc::new(Mutex::new(child))));
  174. });
  175. Ok(())
  176. });
  177. if updater_enabled {
  178. builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
  179. }
  180. builder
  181. .build(tauri::generate_context!())
  182. .expect("error while running tauri application")
  183. .run(|app, event| {
  184. if let RunEvent::Exit = event {
  185. println!("Received Exit");
  186. let Some(server_state) = app.try_state::<ServerState>() else {
  187. println!("Server not running");
  188. return;
  189. };
  190. let Some(server_state) = server_state
  191. .0
  192. .lock()
  193. .expect("Failed to acquire mutex lock")
  194. .take()
  195. else {
  196. println!("Server state missing");
  197. return;
  198. };
  199. let _ = server_state.kill();
  200. println!("Killed server");
  201. }
  202. });
  203. }