lib.rs 7.8 KB

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