From c69b9b939cd7832a60854b6fe7e65e17bd82ac87 Mon Sep 17 00:00:00 2001 From: Swarnim Arun Date: Tue, 31 Oct 2023 19:22:34 +0530 Subject: [PATCH 01/60] fix: gracefully handle process kill (#442) --- src-tauri/Cargo.lock | 86 ++++++++++++++++ src-tauri/Cargo.toml | 2 + src-tauri/src/controller_binaries.rs | 148 +++++++++++++++++++-------- src-tauri/src/download.rs | 10 +- src-tauri/src/errors.rs | 31 ++++++ src-tauri/src/main.rs | 100 ++++++++++++++---- src-tauri/src/utils.rs | 79 +++++++------- src/controller/binariesController.ts | 5 +- 8 files changed, 354 insertions(+), 107 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e7f8eb13..b7f34784 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -457,6 +457,30 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -513,6 +537,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ctrlc" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" +dependencies = [ + "nix", + "windows-sys 0.48.0", +] + [[package]] name = "darling" version = "0.20.3" @@ -639,6 +673,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "embed-resource" version = "2.4.0" @@ -1975,6 +2015,15 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2450,6 +2499,7 @@ name = "prem-app" version = "0.1.2" dependencies = [ "chrono", + "ctrlc", "futures", "log", "pretty_env_logger", @@ -2458,6 +2508,7 @@ dependencies = [ "serde", "serde_json", "sys-info", + "sysinfo", "tauri", "tauri-build", "thiserror", @@ -2647,6 +2698,26 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -3402,6 +3473,21 @@ dependencies = [ "libc", ] +[[package]] +name = "sysinfo" +version = "0.29.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + [[package]] name = "system-configuration" version = "0.5.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 677800c2..142d29b6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -7,11 +7,13 @@ [dependencies] chrono = "0.4.31" + ctrlc = "3.4.1" log = "0.4.20" pretty_env_logger = "0.5.0" sentry-tauri = "0.2" serde_json = "1.0" sys-info = "0.9.1" + sysinfo = "0.29.10" thiserror = "1.0.49" [dependencies.futures] diff --git a/src-tauri/src/controller_binaries.rs b/src-tauri/src/controller_binaries.rs index 8c6eecc5..93934085 100644 --- a/src-tauri/src/controller_binaries.rs +++ b/src-tauri/src/controller_binaries.rs @@ -1,18 +1,27 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use crate::download::Downloader; -use crate::errors::{Context, Result}; -use crate::{Service, SharedState}; -use std::collections::HashMap; +use crate::{ + download::Downloader, + err, + errors::{Context, Result}, + logerr, Service, SharedState, +}; + use std::path::PathBuf; use std::time::Duration; -use sys_info::mem_info; +use std::{collections::HashMap, sync::Arc}; use futures::future; + +use sys_info::mem_info; +use sysinfo::{Pid, ProcessExt, ProcessRefreshKind, RefreshKind, SystemExt}; + use tauri::{AppHandle, Runtime, State, Window}; + +use tokio::process::{Child, Command}; use tokio::time::interval; -use tokio::{fs, process::Command}; +use tokio::{fs, sync::Mutex}; #[tauri::command(async)] pub async fn download_service( @@ -50,7 +59,7 @@ pub async fn download_service( #[tauri::command(async)] pub async fn start_service( service_id: String, - state: State<'_, SharedState>, + state: State<'_, Arc>, app_handle: AppHandle, ) -> Result<()> { let service_dir = app_handle @@ -71,7 +80,7 @@ pub async fn start_service( // Check if service is already running let running_services_guard = state.running_services.lock().await; if running_services_guard.contains_key(&service_id) { - Err(format!("Service with `{service_id}` already exist"))? + err!("Service with `{service_id}` already exist") } drop(running_services_guard); @@ -95,7 +104,7 @@ pub async fn start_service( let binary_path = PathBuf::from(&service_dir).join(&serve_command_vec[0]); log::info!("binary_path: {:?}", binary_path); if !binary_path.exists() { - Err(format!("invalid binary for `{service_id}`"))? + err!("invalid binary for `{service_id}`") } // Extract the arguments with different delimiters let args: Vec = serve_command_vec[1..] @@ -158,32 +167,78 @@ pub async fn start_service( } #[tauri::command(async)] -pub async fn stop_service(service_id: String, state: State<'_, SharedState>) -> Result<()> { - let mut running_services_guard = state.running_services.lock().await; - if let Some(mut child) = running_services_guard.remove(&service_id) { - match child.kill().await { - Ok(_) => { - log::info!("Child process killed: {}", service_id); - } - Err(e) => { - log::error!("Failed to kill child process: {}", e); - } - } +pub async fn stop_service(service_id: String, state: State<'_, Arc>) -> Result<()> { + let running_services = &state.running_services; + let services = &state.services; + _stop_service(service_id.as_str(), running_services, services).await +} + +async fn _stop_service( + service_id: &str, + running_services: &Mutex>, + services: &Mutex>, +) -> Result<()> { + log::info!("stopping service service_id = {service_id}"); + let mut running_services_guard = running_services.lock().await; + let Some(mut child) = running_services_guard.remove(service_id) else { + err!("Service not running") + }; + // kill the process gracefully using SIGTERM/SIGINT + let Some(pid) = child.id() else { + err!("Service couldn't be stopped: {}", service_id) + }; + log::info!("service pid = {pid}"); + let system = sysinfo::System::new_with_specifics( + RefreshKind::new().with_processes(ProcessRefreshKind::new()), + ); + let process = system + .process(Pid::from(pid as usize)) + .with_context(|| format!("Process pid({}) invalid", pid))?; + log::info!( + "terminating service: process_name({}) process_id({})", + process.name(), + process.pid() + ); + if !process + .kill_with(sysinfo::Signal::Term) + .with_context(|| format!("Couldn't send terminate signal to process(pid: {}).", pid))? + { + err!("Failed to kill the process"); } - let mut registry_lock = state.services.lock().await; - if let Some(service) = registry_lock.get_mut(&service_id) { + let mut registry_lock = services.lock().await; + if let Some(service) = registry_lock.get_mut(service_id) { service.running = Some(false); } + // wait for process to properly exit + if let Ok(_exit_code) = child.try_wait() { + log::info!("service stopped!"); + } else if let Ok(_exit_code) = child.wait().await { + log::info!("service stopped!"); + } Ok(()) } -pub fn stop_all_services(state: tauri::State<'_, SharedState>) { +pub fn stop_all_services(state: Arc) { + log::info!("Stopping all services"); tauri::async_runtime::block_on(async move { - let services = state.running_services.lock().await; - for service_id in services.keys() { - _ = stop_service(service_id.clone(), state.clone()).await; + let keys = state + .running_services + .lock() + .await + .keys() + .cloned() + .collect::>(); + for service_id in keys { + logerr!( + _stop_service( + service_id.as_str(), + &state.running_services, + &state.services + ) + .await + ); } - }) + }); } #[tauri::command(async)] @@ -213,7 +268,7 @@ pub async fn get_logs_for_service(service_id: String, app_handle: AppHandle) -> #[tauri::command] pub async fn get_services( - state: State<'_, SharedState>, + state: State<'_, Arc>, app_handle: AppHandle, ) -> Result> { let mut services = Vec::new(); @@ -254,7 +309,7 @@ pub async fn get_services( #[tauri::command(async)] pub async fn get_service_by_id( service_id: String, - state: State<'_, SharedState>, + state: State<'_, Arc>, app_handle: AppHandle, ) -> Result { let services = state.services.lock().await; @@ -265,12 +320,15 @@ pub async fn get_service_by_id( // Dynamic service state #[tauri::command(async)] -pub async fn get_running_services(state: State<'_, SharedState>) -> Result> { +pub async fn get_running_services(state: State<'_, Arc>) -> Result> { let services = state.running_services.lock().await; return Ok(services.keys().cloned().collect()); } -pub async fn is_service_running(service_id: &str, state: &State<'_, SharedState>) -> Result { +pub async fn is_service_running( + service_id: &str, + state: &State<'_, Arc>, +) -> Result { let running_services = state.running_services.lock().await; return Ok(running_services.contains_key(service_id)); } @@ -279,9 +337,9 @@ pub async fn is_service_downloaded(service: &Service, app_handle: &AppHandle) -> let service_dir = app_handle .path_resolver() .app_data_dir() - .expect("failed to resolve app data dir") + .with_context(|| "Failed to resolve app data dir")? .join("models") - .join(&service.id.as_ref().unwrap()); + .join(service.get_id_ref()?); let mut downloaded = true; for file in &service.weights_files.clone().unwrap() { let file_path = service_dir.join(file); @@ -304,11 +362,19 @@ pub async fn is_service_downloaded(service: &Service, app_handle: &AppHandle) -> .head(url) .send() .await - .map_err(|_| format!("Failed HEAD request: {}", url)) - .unwrap(); + .map_err(|_| format!("Failed HEAD request: {}", url))?; if response.status().is_success() { if let Some(remote_file_size) = response.headers().get("Content-Length") { - let parsed_size = remote_file_size.to_str().unwrap().parse::().unwrap(); + let parsed_size = remote_file_size + .to_str() + .with_context(|| "Header value remote_file_size not utf-8")? + .parse::() + .with_context(|| { + format!( + "Failed to parse header-value({:?}) as u64", + remote_file_size.to_str() + ) + })?; // println!("Content-Length: {:?}", parsed_size); if file_size_on_disk != parsed_size { downloaded = false; @@ -359,7 +425,7 @@ pub fn get_base_url(service: &Service) -> Result { pub async fn update_service_with_dynamic_state( service: &mut Service, - state: &State<'_, SharedState>, + state: &State<'_, Arc>, app_handle: &AppHandle, ) -> Result { let is_service_downloaded = is_service_downloaded(service, &app_handle).await?; @@ -367,7 +433,7 @@ pub async fn update_service_with_dynamic_state( let has_enough_total_memory = has_enough_total_memory(service).await?; let has_enough_storage = has_enough_storage().await?; let is_supported = is_supported().await?; - let is_service_running = is_service_running(&service.id.as_ref().unwrap(), &state).await?; + let is_service_running = is_service_running(service.get_id_ref()?, &state).await?; let base_url = get_base_url(service)?; service.downloaded = Some(is_service_downloaded); service.enough_memory = Some(has_enough_free_memory); @@ -383,9 +449,9 @@ pub async fn update_service_with_dynamic_state( pub async fn get_system_stats() -> Result> { Ok(HashMap::new()) } + #[tauri::command(async)] pub async fn get_service_stats(service_id: String) -> Result> { - log::info!("service_id: {}", service_id); Ok(HashMap::new()) } #[tauri::command(async)] @@ -394,8 +460,8 @@ pub async fn get_gpu_stats() -> Result> { } #[tauri::command(async)] -pub async fn add_service(service: Service, state: State<'_, SharedState>) -> Result<()> { +pub async fn add_service(service: Service, state: State<'_, Arc>) -> Result<()> { let mut services_guard = state.services.lock().await; - services_guard.insert(service.id.clone().unwrap(), service.clone()); + services_guard.insert(service.get_id()?, service); Ok(()) } diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs index ad8210f8..e59526a2 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/download.rs @@ -1,7 +1,7 @@ use crate::errors::{Context, Result}; use std::collections::HashMap; -use crate::{utils, SharedState}; +use crate::{err, utils, SharedState}; use futures::StreamExt; use serde::Serialize; use tauri::{Manager, Runtime, Window}; @@ -175,11 +175,7 @@ impl Downloader { // Check the status for errors. if !res.status().is_success() { - Err(format!( - "GET Request: ({}): ({})", - res.status(), - url.as_ref() - ))? + err!("GET Request: ({}): ({})", res.status(), url.as_ref()); } // Prepare the destination directories @@ -211,7 +207,7 @@ impl Downloader { // Retrieve chunk. let mut chunk = match item { Ok(chunk) => chunk, - Err(e) => Err(format!("Error while downloading: {:?}", e))?, + Err(e) => err!("Error while downloading: {e:?}"), }; let chunk_size = chunk.len() as u64; diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index a657e7fe..0e37cd0e 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -40,3 +40,34 @@ impl serde::Serialize for Error { serializer.serialize_str(self.to_string().as_ref()) } } + +#[macro_export] +macro_rules! err { + ($($t:tt)*) => {{ + Err(format!($($t)*))? + }}; +} + +#[macro_export] +macro_rules! logerr { + ($ar:expr $(,)+ $($t:tt)+) => {{ + if let Err(e) = $ar { + log::error!("{e:?}"); + log::error!($($t)*); + } + }}; + ($ar:expr) => {{ + if let Err(e) = $ar { + log::error!("{e:?}"); + } + }}; +} + +#[macro_export] +macro_rules! logsome { + ($ar:expr, $($t:tt)*) => {{ + if ($ar).is_none() { + log::error!($($t)*); + } + }}; +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 77864a7f..8ab1dfb1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,14 +7,18 @@ mod errors; mod swarm; mod utils; +use crate::controller_binaries::stop_all_services; + +use std::{collections::HashMap, env, ops::Deref, str, sync::Arc}; + use sentry_tauri::sentry; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, env, str}; use tauri::{ AboutMetadata, CustomMenuItem, Manager, Menu, MenuItem, RunEvent, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, WindowEvent, }; -use tokio::{process::Child, sync::Mutex}; +use tokio::process::Child; +use tokio::sync::Mutex; #[derive(Debug, Default)] pub struct SharedState { @@ -73,6 +77,22 @@ pub struct Service { supported: Option, } +impl Service { + fn get_id(&self) -> errors::Result { + use errors::Context; + self.id + .clone() + .with_context(|| format!("Service doesn't contain a valid id\n{:#?}", self)) + } + // ref to String is used as it's more generally coerce-able + fn get_id_ref(&self) -> errors::Result<&String> { + use errors::Context; + self.id + .as_ref() + .with_context(|| format!("Service doesn't contain a valid id\n{:#?}", self)) + } +} + #[derive(Debug, Deserialize, Serialize, Clone)] struct ModelInfo { #[serde(rename = "inferenceTime")] @@ -91,12 +111,11 @@ struct ModelInfo { } fn main() { - // Sentry let client = sentry::init(( "https://b98405fd0e4cc275b505645d293d23a5@o4506111848808448.ingest.sentry.io/4506111925223424", sentry::ClientOptions { release: sentry::release_name!(), - debug: true, + debug: false, // this outputs dsn to stdout on sentry init ..Default::default() }, )); @@ -105,8 +124,11 @@ fn main() { let _guard = sentry_tauri::minidump::init(&client); // Everything after here runs in only the app process + // TODO: consider directly pushing logs to sentry (sentry-sdk provides + // log integration) for release builds + // initialize logger - pretty_env_logger::formatted_timed_builder() + pretty_env_logger::formatted_builder() .format(|buf, record| { use std::io::Write; writeln!( @@ -164,11 +186,11 @@ fn main() { let system_tray = SystemTray::new().with_menu(tray_menu); - let state = SharedState::default(); - #[allow(unused_mut)] - let mut app = tauri::Builder::default() + let state = std::sync::Arc::new(SharedState::default()); + + let app = tauri::Builder::default() .plugin(sentry_tauri::plugin()) - .manage(state) + .manage(state.clone()) .invoke_handler(tauri::generate_handler![ controller_binaries::start_service, controller_binaries::stop_service, @@ -192,7 +214,9 @@ fn main() { .menu(menu) .on_menu_event(|event| match event.menu_item_id() { "quit" => { - controller_binaries::stop_all_services(event.window().state()); + controller_binaries::stop_all_services( + event.window().state::>().deref().clone(), + ); event.window().close().unwrap(); } "close" => { @@ -204,17 +228,25 @@ fn main() { .on_system_tray_event(|app, event| match event { SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { "hide" => { - let window = app.get_window("main").unwrap(); - window.hide().unwrap(); + let Some(window) = app.get_window("main") else { + log::error!("Couldn't get window from for label 'main'"); + return; + }; + logerr!(window.hide()); } "quit" => { - controller_binaries::stop_all_services(app.state()); + controller_binaries::stop_all_services( + app.state::>().deref().clone(), + ); app.exit(0); } "show" => { - let window = app.get_window("main").unwrap(); - window.set_focus().unwrap(); - window.show().unwrap(); + let Some(window) = app.get_window("main") else { + log::error!("Couldn't get window from for label 'main'"); + return; + }; + logerr!(window.set_focus()); + logerr!(window.show()); } _ => {} }, @@ -224,22 +256,50 @@ fn main() { tauri::async_runtime::block_on(async move { utils::fetch_services_manifests( "https://raw.githubusercontent.com/premAI-io/prem-registry/dev/manifests.json", - &app.state::(), + app.state::>().deref().clone(), ) .await .expect("Failed to fetch and save services manifests"); }); Ok(()) }) + .on_window_event(|ev| { + if matches!(ev.event(), WindowEvent::Destroyed) { + stop_all_services(ev.window().state::>().deref().clone()); + } + }) .build(tauri::generate_context!()) - .expect("error while running tauri application"); + .expect("Error while building tauri application"); + + { + let app_handle = app.handle(); + let s = state.clone(); + std::panic::set_hook(Box::new(move |_| { + stop_all_services(s.clone()); + app_handle.exit(-1); + })); + + let app_handle = app.handle(); + let s = state.clone(); + ctrlc::set_handler(move || { + stop_all_services(s.clone()); + app_handle.exit(-1); + }) + .expect("Error setting Ctrl-C handler"); + } app.run(|app_handle, e| match e { // Triggered when a window is trying to close RunEvent::WindowEvent { label, event, .. } => { match event { WindowEvent::CloseRequested { api, .. } => { - app_handle.get_window(&label).unwrap().hide().unwrap(); + logsome!( + app_handle.get_window(&label).map(|e| logerr!( + e.hide(), + "Failed to hide window with label({label:?})" + )), + "Failed to get app window with label({label:?})" + ); // use the exposed close api, and prevent the event loop to close api.prevent_close(); } @@ -247,5 +307,5 @@ fn main() { } } _ => {} - }) + }); } diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 55fee848..4cacfcd2 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,51 +1,60 @@ -use crate::errors::Result; -use crate::{Service, SharedState}; +use crate::errors::{Context, Result}; +use crate::{err, Service, SharedState}; use reqwest::get; use std::collections::HashMap; -use tauri::State; +use std::sync::Arc; -pub async fn fetch_services_manifests(url: &str, state: &State<'_, SharedState>) -> Result<()> { - let response = get(url).await.expect("Failed to fetch registry"); - let services = response.json::>().await.unwrap(); +pub async fn fetch_services_manifests(url: &str, state: Arc) -> Result<()> { + let response = get(url) + .await + .with_context(|| format!("Couldn't fetch the manifest from {url:?}"))?; + let services = response + .json::>() + .await + .with_context(|| "Failed to parse response to list of services")?; let mut services_guard = state.services.lock().await; - let service_ids = services_guard.keys().cloned().collect::>(); - for service in services { - if !service_ids.contains(&service.id.clone().unwrap()) { - services_guard.insert(service.id.clone().unwrap(), service); - } - } + // TODO: discuss why do we need global ids, why not use uuids and generate them on each load + *services_guard = services + .into_iter() + // removes services without id + .filter_map(|x| Some((x.id.clone()?, x))) + // removes duplicate services + .collect(); Ok(()) } -pub fn is_aarch64() -> bool { +pub const fn is_aarch64() -> bool { cfg!(target_arch = "aarch64") } -pub fn is_x86_64() -> bool { +pub const fn is_x86_64() -> bool { cfg!(target_arch = "x86_64") } +pub const fn is_macos() -> bool { + cfg!(target_os = "macos") +} + +pub const fn is_unix() -> bool { + cfg!(target_family = "unix") +} + pub fn get_binary_url(binaries_url: &HashMap>) -> Result { - let mut binary_url = "".to_string(); - if is_aarch64() { - binary_url = binaries_url - .get("aarch64-apple-darwin") - .unwrap() - .clone() - .unwrap_or_else(|| "Unsupported architecture".to_string()) - } else if is_x86_64() { - binary_url = binaries_url - .get("x86_64-apple-darwin") - .unwrap() - .clone() - .unwrap_or_else(|| "Unsupported architecture".to_string()) - } - if binary_url == "Unsupported architecture" { - binary_url = binaries_url - .get("universal-apple-darwin") - .unwrap() - .clone() - .unwrap_or_else(|| Err("Unsupported architecture").unwrap()) + if !is_unix() && !is_macos() { + err!("Unsupported OS") + } else if is_macos() { + if is_x86_64() { + binaries_url.get("x86_64-apple-darwin") + } else if is_aarch64() { + binaries_url.get("aarch64-apple-darwin") + } else { + err!("Unsupported architecture") + } + .or_else(|| binaries_url.get("universal-apple-darwin")) + .with_context(|| "No binary available for platform")? + .clone() + .with_context(|| "Service not supported on your platform") + } else { + err!("Unsupported platform") } - Ok(binary_url) } diff --git a/src/controller/binariesController.ts b/src/controller/binariesController.ts index bccffa7a..8ebca0af 100644 --- a/src/controller/binariesController.ts +++ b/src/controller/binariesController.ts @@ -13,10 +13,7 @@ class BinariesController extends AbstractServiceController { async restart(serviceId: string): Promise { await this.stop(serviceId); - const services: string[] = await invoke("get_running_services"); - while (!services.includes(serviceId)) { - await this.start(serviceId); - } + await this.start(serviceId); } async stop(serviceId: string): Promise { From ca2cac10eaa0ba76be0be5b533a940b8637b9a02 Mon Sep 17 00:00:00 2001 From: Janaka-Steph Date: Tue, 31 Oct 2023 15:49:50 +0100 Subject: [PATCH 02/60] Display red dot for new feature (#451) * Display red dot for new feature * Beta tag --- src/modules/settings/components/SwarmMode.tsx | 4 +++- src/shared/components/DashboardSidebar.tsx | 5 +++++ src/shared/components/NavLinkItem.tsx | 16 +++++++++++++--- src/shared/store/setting.ts | 2 ++ src/shared/types/index.ts | 3 +++ tailwind.config.js | 1 + 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/modules/settings/components/SwarmMode.tsx b/src/modules/settings/components/SwarmMode.tsx index ddac8d8a..bb631795 100644 --- a/src/modules/settings/components/SwarmMode.tsx +++ b/src/modules/settings/components/SwarmMode.tsx @@ -103,7 +103,9 @@ const SwarmMode = () => { return isSwarmSupported ? ( <>
-

Prem Network

+

+ Prem Network BETA +

{swarmMode === Swarm.Active ? ( <> diff --git a/src/shared/components/DashboardSidebar.tsx b/src/shared/components/DashboardSidebar.tsx index 6afe5118..be193c97 100644 --- a/src/shared/components/DashboardSidebar.tsx +++ b/src/shared/components/DashboardSidebar.tsx @@ -1,5 +1,7 @@ import brandLogo from "assets/images/brand-logo.svg"; +import useSettingStore from "../store/setting"; + import DashboardIcon from "./DashboardIcon"; import DocumentationIcon from "./DocumentationIcon"; import NavLinkContainer from "./NavLinkContainer"; @@ -7,6 +9,8 @@ import NavLinkItem from "./NavLinkItem"; import SettingIcon from "./SettingIcon"; const DashboardSidebar = () => { + const newFeature = useSettingStore((state) => state.newFeature); + return (
@@ -29,6 +33,7 @@ const DashboardSidebar = () => { to="/settings" label="Settings" icon={} + newFeature={newFeature} />
diff --git a/src/shared/components/NavLinkItem.tsx b/src/shared/components/NavLinkItem.tsx index df38535b..efe08108 100644 --- a/src/shared/components/NavLinkItem.tsx +++ b/src/shared/components/NavLinkItem.tsx @@ -1,21 +1,31 @@ -import { NavLink } from "react-router-dom"; import "react-tooltip/dist/react-tooltip.css"; + +import clsx from "clsx"; +import { NavLink } from "react-router-dom"; import { Tooltip } from "react-tooltip"; import type { NavLinkItemProps } from "shared/types"; import { useMediaQuery } from "usehooks-ts"; -const NavLinkItem = ({ to, icon, label, target }: NavLinkItemProps) => { +import useSettingStore from "../store/setting"; + +const NavLinkItem = ({ to, icon, label, target, newFeature = false }: NavLinkItemProps) => { const onlyDesktopShow = useMediaQuery("(min-width: 768px)"); + const setNewFeature = useSettingStore((state) => state.setNewFeature); + return (
  • (isActive ? "active" : "")} + className={({ isActive }) => clsx({ active: isActive }, "relative")} data-tooltip-id="sidebar-tooltip" data-tooltip-content={label} + onClick={() => newFeature && setNewFeature(false)} > + {newFeature ? ( +
    + ) : null} {icon} {label}
    diff --git a/src/shared/store/setting.ts b/src/shared/store/setting.ts index 61117e5d..d5148202 100644 --- a/src/shared/store/setting.ts +++ b/src/shared/store/setting.ts @@ -86,6 +86,8 @@ const useSettingStore = create()( "removeAllServiceAsDownloading", ); }, + newFeature: true, + setNewFeature: (newFeature: boolean) => set(() => ({ newFeature }), false, "setNewFeature"), swarmMode: Swarm.Inactive, setSwarmMode: (swarmMode: Swarm) => set(() => ({ swarmMode }), false, "setSwarmMode"), swarmInfo: null, diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index d3e73ee9..305b0eae 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -33,6 +33,7 @@ export type NavLinkItemProps = { label: string; to: string; target?: "_blank"; + newFeature?: boolean; }; export enum Swarm { @@ -108,6 +109,8 @@ export type SettingStore = { addServiceAsDownloading: (serviceId: string) => void; removeServiceAsDownloading: (serviceId: string) => void; removeAllServiceAsDownloading: () => void; + newFeature: boolean; + setNewFeature: (newFeature: boolean) => void; swarmMode: Swarm; setSwarmMode: (mode: Swarm) => void; swarmInfo: SwarmInfo | null; diff --git a/tailwind.config.js b/tailwind.config.js index 01287e22..50367fa6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -31,6 +31,7 @@ module.exports = { }, ok: "#2ED291", warning: "#F9B96D", + danger: "#C91432", }, extend: { fontFamily: { From db06915ca9b9f6a7639f083c764e61500ff4d999 Mon Sep 17 00:00:00 2001 From: tiero <3596602+tiero@users.noreply.github.com> Date: Wed, 1 Nov 2023 03:15:55 +0100 Subject: [PATCH 03/60] gha: workflow_dispatch with manual version and branch name --- .github/workflows/on-release.yaml | 127 ++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .github/workflows/on-release.yaml diff --git a/.github/workflows/on-release.yaml b/.github/workflows/on-release.yaml new file mode 100644 index 00000000..0b5705a6 --- /dev/null +++ b/.github/workflows/on-release.yaml @@ -0,0 +1,127 @@ +name: Release on user input + +on: + workflow_dispatch: + inputs: + branchName: + description: 'Branch Name' + required: true + version: + description: 'New version' + required: true + +jobs: + publish-tauri: + permissions: write-all + strategy: + fail-fast: false + matrix: + platform: [macos-latest] + + runs-on: ${{ matrix.platform }} + + steps: + + - name: View branch name + run: | + echo "Branch name: ${{ github.event.inputs.branchName }}" + + - name: View version + run: | + echo "Tag: ${{ github.event.inputs.version }}" + + - name: Version as Number + id: next_version + run: | + tag=${{ github.event.inputs.version }} + echo "version=${tag:1}" >> $GITHUB_OUTPUT + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Rust setup + uses: dtolnay/rust-toolchain@stable + + - name: install dependencies (ubuntu only) + if: matrix.platform == 'ubuntu-20.04' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Install missing Rust target for universal Mac build + if: matrix.platform == 'macos-latest' + run: rustup target add aarch64-apple-darwin + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: "./src-tauri -> target" + + - name: Sync node version and setup cache + uses: actions/setup-node@v3 + with: + node-version: "lts/*" + cache: "npm" + + - name: Install frontend dependencies + run: npm install + + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ^1.20.1 + id: go + + - name: Install dasel + run: | + go install github.com/tomwright/dasel/cmd/dasel@latest + + + - name: Update with latest branch + run: | + git config --local user.email "$(git log --format='%ae' HEAD^!)" + git config --local user.name "$(git log --format='%an' HEAD^!)" + git config pull.rebase true + git stash + git fetch origin ${{ github.event.inputs.branchName }} + git pull origin ${{ github.event.inputs.branchName }} + git stash pop || true + + - name: Increment version + run: | + dasel put string -f package.json ".version" "${{ steps.next_version.outputs.version }}" + dasel put string -f src-tauri/tauri.conf.json ".package.version" "${{ steps.next_version.outputs.version }}" + dasel put string -f src-tauri/Cargo.toml ".package.version" "${{ steps.next_version.outputs.version }}" + + - name: Build the app + uses: tauri-apps/tauri-action@dev + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + with: + tagName: ${{ github.event.inputs.version }} + releaseName: "PremAI App ${{ github.event.inputs.version }}" + releaseBody: "See the assets to download and install this version." + releaseDraft: false + includeDebug: true + updaterJsonKeepUniversal: true + args: ${{matrix.platform == 'ubuntu-20.04' && '--target x86_64-unknown-linux-gnu' || '--target universal-apple-darwin'}} + + + # Commit package.json, tauri.conf.json and Cargo.toml to master + - name: Commit & Push + continue-on-error: true + run: | + git add . + git commit -m "${{ github.event.inputs.version }}" + git push origin HEAD:${{ github.event.inputs.branchName }} + + From 5e53fd09ba8235817959020aee36f844d9264fce Mon Sep 17 00:00:00 2001 From: tiero <3596602+tiero@users.noreply.github.com> Date: Wed, 1 Nov 2023 13:22:41 +0100 Subject: [PATCH 04/60] chore: add docker-compose to test app, daemon and gateway --- docker-compose.gateway.yml | 115 +++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docker-compose.gateway.yml diff --git a/docker-compose.gateway.yml b/docker-compose.gateway.yml new file mode 100644 index 00000000..1e88cb72 --- /dev/null +++ b/docker-compose.gateway.yml @@ -0,0 +1,115 @@ +version: '3.7' +services: + + premapp: + container_name: premapp + build: . + environment: + - VITE_DESTINATION=browser + - VITE_IS_PACKAGED=true + - VITE_PROXY_ENABLED=true + labels: + - "traefik.enable=true" + - "traefik.http.routers.premapp-http.rule=PathPrefix(`/`)" + - "traefik.http.routers.premapp-http.entrypoints=web" + - "traefik.http.services.premapp.loadbalancer.server.port=8080" + ports: + - "8085:8080" + restart: unless-stopped + + premd: + container_name: premd + image: ghcr.io/premai-io/premd:7af3782b00f6176b34840f608327c051502511ce + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - PREM_REGISTRY_URL=https://raw.githubusercontent.com/premAI-io/prem-registry/main/manifests.json + - PROXY_ENABLED=True + - DOCKER_NETWORK=prem-app_default + labels: + - "traefik.enable=true" + - "traefik.http.routers.premd.rule=PathPrefix(`/premd`)" + - "traefik.http.middlewares.premd-strip-prefix.stripprefix.prefixes=/premd" + - "traefik.http.routers.premd.middlewares=premd-strip-prefix" + ports: + - "8084:8000" + restart: unless-stopped + + + + traefik: + container_name: traefik + image: traefik:v2.4 + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--accesslog=true" + - "--ping" + - "--entrypoints.web.address=:80" + ports: + - "80:80" + - "8080:8080" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - traefik-letsencrypt:/letsencrypt + depends_on: + - dnsd + restart: unless-stopped + + dnsd: + container_name: dnsd + image: ghcr.io/premai-io/dnsd:latest + labels: + - "traefik.enable=true" + - "traefik.http.routers.dnsd.rule=PathPrefix(`/dnsd`)" + - "traefik.http.middlewares.dnsd-strip-prefix.stripprefix.prefixes=/dnsd" + - "traefik.http.routers.dnsd.middlewares=dnsd-strip-prefix" + depends_on: + - dnsd-db-pg + - authd + environment: + PREM_GATEWAY_DNS_DB_USER: root + PREM_GATEWAY_DNS_DB_PASS: secret + PREM_GATEWAY_DNS_DB_NAME: dnsd-db + PREM_GATEWAY_DNS_DB_HOST: dnsd-db-pg + ports: + - "8082:8080" + restart: unless-stopped + + dnsd-db-pg: + container_name: dnsd-db-pg + image: postgres:14.7 + ports: + - "5432:5432" + environment: + POSTGRES_USER: root + POSTGRES_PASSWORD: secret + POSTGRES_DB: dnsd-db + volumes: + - dnsd-pg-data:/var/lib/postgresql/data + restart: unless-stopped + + authd: + container_name: authd + image: ghcr.io/premai-io/authd:latest + ports: + - "8081:8080" + restart: unless-stopped + + controllerd: + container_name: controllerd + image: ghcr.io/premai-io/controllerd:latest + ports: + - "8083:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + user: root + environment: + LETSENCRYPT_PROD: false + SERVICES: premd,premapp + restart: unless-stopped + +volumes: + dnsd-pg-data: + traefik-letsencrypt: \ No newline at end of file From d6e153e8d8655ab64e2159cc0585093788efa29d Mon Sep 17 00:00:00 2001 From: Marco Argentieri <3596602+tiero@users.noreply.github.com> Date: Wed, 1 Nov 2023 14:15:36 +0100 Subject: [PATCH 05/60] fix: match premd routes with slash in docker controller (#455) --- src/controller/dockerController.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controller/dockerController.ts b/src/controller/dockerController.ts index e1b82fd1..094d3ba6 100644 --- a/src/controller/dockerController.ts +++ b/src/controller/dockerController.ts @@ -62,7 +62,7 @@ class DockerController extends AbstractServiceController { } async getServices(): Promise { - const response = await api().get("v1/services"); + const response = await api().get("v1/services/"); return response.data; } @@ -76,17 +76,17 @@ class DockerController extends AbstractServiceController { } async getSystemStats(): Promise> { - const response = await api().get("v1/stats-all"); + const response = await api().get("v1/stats-all/"); return response.data; } async getGPUStats(): Promise> { - const response = await api().get("v1/gpu-stats-all"); + const response = await api().get("v1/gpu-stats-all/"); return response.data; } async getInterfaces(): Promise { - const response = await api().get("v1/interfaces"); + const response = await api().get("v1/interfaces/"); return response.data; } From 849bab415b60a1cb4e3f7e6d88a5236ec41cd74a Mon Sep 17 00:00:00 2001 From: Janaka-Steph Date: Wed, 1 Nov 2023 16:56:17 +0100 Subject: [PATCH 06/60] Remove isFetching (#460) --- src/modules/service-detail/components/ServiceDetail.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/modules/service-detail/components/ServiceDetail.tsx b/src/modules/service-detail/components/ServiceDetail.tsx index e5e29647..010bac5b 100644 --- a/src/modules/service-detail/components/ServiceDetail.tsx +++ b/src/modules/service-detail/components/ServiceDetail.tsx @@ -26,7 +26,7 @@ const ServiceDetail = () => { serviceType: Service["serviceType"]; }>(); const navigate = useNavigate(); - const { data: service, isLoading, isFetching, refetch } = useGetService(serviceId!, serviceType!); + const { data: service, isLoading, refetch } = useGetService(serviceId!, serviceType!); const { data: interfaces } = useInterfaces(); @@ -39,8 +39,7 @@ const ServiceDetail = () => { navigate("/"); }; - if (isFetching || isLoading || !service || Object.keys(service ?? {}).length === 0) - return ; + if (isLoading || !service || Object.keys(service ?? {}).length === 0) return ; const status = getServiceStatus(service); From 38559bf9a5cb92bf5fdbf4a9823f91efc4e62f40 Mon Sep 17 00:00:00 2001 From: tiero <3596602+tiero@users.noreply.github.com> Date: Wed, 1 Nov 2023 18:23:40 +0100 Subject: [PATCH 07/60] GHA: split Tauri and Docker in two workflows (#462) --- .github/workflows/on-pull-req.yaml | 2 +- .github/workflows/on-push-server.yaml | 32 ------------ .../on-workflow-dispatch-docker.yaml | 52 +++++++++++++++++++ ...e.yaml => on-workflow-dispatch-tauri.yaml} | 13 +++-- 4 files changed, 62 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/on-push-server.yaml create mode 100644 .github/workflows/on-workflow-dispatch-docker.yaml rename .github/workflows/{on-release.yaml => on-workflow-dispatch-tauri.yaml} (91%) diff --git a/.github/workflows/on-pull-req.yaml b/.github/workflows/on-pull-req.yaml index 153f40b7..e6874174 100644 --- a/.github/workflows/on-pull-req.yaml +++ b/.github/workflows/on-pull-req.yaml @@ -3,7 +3,7 @@ on: pull_request: branches: [main] jobs: - prettier: + check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/on-push-server.yaml b/.github/workflows/on-push-server.yaml deleted file mode 100644 index 1d740cf1..00000000 --- a/.github/workflows/on-push-server.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: On Push Server -on: - push: {branches: [server]} -jobs: - push-docker-image: - runs-on: ubuntu-latest - env: - DOCKER_CLI_EXPERIMENTAL: enabled - steps: - - uses: actions/checkout@v4 - - name: metadata - id: metadata - run: | - git fetch --all --tags - TAG=$(git describe --tags) - echo "tag=${TAG/-*}" >> $GITHUB_OUTPUT - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - with: - install: true - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build & push prem - run: >- - docker buildx build --push - --file Dockerfile - --tag ghcr.io/premai-io/prem-app:latest - --tag ghcr.io/premai-io/prem-app:${{ steps.metadata.outputs.tag }} - --platform linux/arm64,linux/amd64 . diff --git a/.github/workflows/on-workflow-dispatch-docker.yaml b/.github/workflows/on-workflow-dispatch-docker.yaml new file mode 100644 index 00000000..01d5002d --- /dev/null +++ b/.github/workflows/on-workflow-dispatch-docker.yaml @@ -0,0 +1,52 @@ +name: 🚀 Docker Image + +on: + workflow_dispatch: + inputs: + version: + description: 'Version tag for the Docker image (optional, will use latest Git tag if empty)' + required: false + type: string + tag_as_latest: + description: 'Also tag as latest?' + required: false + default: false + type: boolean + +jobs: + push-docker-image: + runs-on: ubuntu-latest + env: + DOCKER_CLI_EXPERIMENTAL: enabled + steps: + - uses: actions/checkout@v4 + - name: Determine tag + id: tag + run: | + if [ -z "${{ github.event.inputs.version }}" ]; then + git fetch --all --tags + TAG=$(git describe --tags `git rev-list --tags --max-count=1`) + else + TAG=${{ github.event.inputs.version }} + fi + echo "VERSION_TAG=$TAG" >> $GITHUB_ENV + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + with: + install: true + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push + run: | + docker buildx build --push \ + --file Dockerfile \ + --tag ghcr.io/premai-io/prem-app:$VERSION_TAG \ + ${{ github.event.inputs.tag_as_latest == 'true' && '--tag ghcr.io/premai-io/prem-app:latest' || '' }} \ + --platform linux/arm64,linux/amd64 . + shell: /usr/bin/bash -e {0} + env: + DOCKER_CLI_EXPERIMENTAL: enabled + diff --git a/.github/workflows/on-release.yaml b/.github/workflows/on-workflow-dispatch-tauri.yaml similarity index 91% rename from .github/workflows/on-release.yaml rename to .github/workflows/on-workflow-dispatch-tauri.yaml index 0b5705a6..91cd07aa 100644 --- a/.github/workflows/on-release.yaml +++ b/.github/workflows/on-workflow-dispatch-tauri.yaml @@ -1,14 +1,19 @@ -name: Release on user input +name: 🚀 Tauri Dekstop App on: workflow_dispatch: inputs: branchName: - description: 'Branch Name' + description: 'Branch Name you are releasing from' required: true version: - description: 'New version' + description: 'Version tag for the Github Release and the .dmg for MacOS' required: true + release_as_draft: + description: 'Release as Draft' + required: false + default: true + type: boolean jobs: publish-tauri: @@ -110,7 +115,7 @@ jobs: tagName: ${{ github.event.inputs.version }} releaseName: "PremAI App ${{ github.event.inputs.version }}" releaseBody: "See the assets to download and install this version." - releaseDraft: false + releaseDraft: ${{ github.event.inputs.release_as_draft }} includeDebug: true updaterJsonKeepUniversal: true args: ${{matrix.platform == 'ubuntu-20.04' && '--target x86_64-unknown-linux-gnu' || '--target universal-apple-darwin'}} From 5a8676cf247e1710bc73d996b2b9627ba7a5c55c Mon Sep 17 00:00:00 2001 From: nsosio Date: Fri, 27 Oct 2023 14:27:30 +0200 Subject: [PATCH 08/60] split create_env script --- src-tauri/petals/create_env.sh | 6 +- src-tauri/petals/run_swarm.sh | 20 +++++ src-tauri/src/main.rs | 3 +- src-tauri/src/swarm.rs | 78 ++++++++++++------- src/modules/settings/components/SwarmMode.tsx | 2 + src/shared/helpers/utils.ts | 6 +- 6 files changed, 78 insertions(+), 37 deletions(-) create mode 100755 src-tauri/petals/run_swarm.sh diff --git a/src-tauri/petals/create_env.sh b/src-tauri/petals/create_env.sh index b9bc1738..f4b2be0f 100755 --- a/src-tauri/petals/create_env.sh +++ b/src-tauri/petals/create_env.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash prem_appdir="${PREM_APPDIR:-.}" -requirements="$(dirname "$0")/requirements.txt" conda_prefix="$prem_appdir/miniconda" if test ! -x "$conda_prefix/bin/mamba"; then @@ -27,7 +26,4 @@ echo "Using Conda at '$conda_prefix'" export PREM_PYTHON="$conda_prefix/envs/prem_app/bin/python" echo "Ensuring env 'prem_app' exists" -test -x "$PREM_PYTHON" || "$conda_prefix/bin/mamba" create -y -n prem_app python=3.11 - -echo "Installing requirements" -"$PREM_PYTHON" -m pip install -r "$requirements" +test -x "$PREM_PYTHON" || "$conda_prefix/bin/mamba" create -y -n prem_app python=3.11 \ No newline at end of file diff --git a/src-tauri/petals/run_swarm.sh b/src-tauri/petals/run_swarm.sh new file mode 100755 index 00000000..7320b7f2 --- /dev/null +++ b/src-tauri/petals/run_swarm.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Usage: ./run_swarm.sh +# +# Parameters: +# num_blocks: The number of blocks to use. +# public_name: The public name to use. +# model: The model to use. + +prem_python="${PREM_PYTHON:-.}" +requirements="$(dirname "$0")/requirements.txt" + +echo "Installing requirements" +"$prem_python" -m pip install -r "$requirements" + +echo "Running petals" +"$prem_python" -m petals.cli.run_server \ + --num_blocks "$1" \ + --public_name "$2" \ + "$3" \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8ab1dfb1..dab7c914 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -207,7 +207,8 @@ fn main() { swarm::is_swarm_supported, swarm::get_username, swarm::get_petals_models, - swarm::run_swarm_mode, + swarm::create_environment, + swarm::run_swarm, swarm::stop_swarm_mode, swarm::is_swarm_mode_running ]) diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index c7d6340e..ea73fdc6 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -70,63 +70,71 @@ pub fn is_swarm_mode_running() -> bool { return false; } -pub fn create_environment(handle: tauri::AppHandle) -> String { - // Get the application data directory +#[tauri::command(async)] +pub fn create_environment(handle: tauri::AppHandle) { + println!("🐍 Creating the environment..."); + let petals_path = get_petals_path(handle); + let app_data_dir = tauri::api::path::home_dir().expect("🙈 Failed to get app data directory"); let app_data_dir = app_data_dir.join(".config/prem"); let app_data_dir = app_data_dir .to_str() .expect("🙈 Failed to convert app data dir path to str"); - // Get create env path - let binding = handle - .path_resolver() - .resolve_resource("petals") - .expect("🙈 Failed to find `create_env.sh`"); - let petals_path = binding - .to_str() - .expect("🙈 Failed to convert petals path to str"); - let python = format!("{app_data_dir}/miniconda/envs/prem_app/bin/python"); - // Set env variables let mut env = HashMap::new(); env.insert("PREM_APPDIR".to_string(), app_data_dir.to_string()); env.insert("PREM_PYTHON".to_string(), python.clone()); - // Run the bash script let _ = Command::new("sh") .args([format!("{petals_path}/create_env.sh")]) - .envs(env) + .envs(env.clone()) .output() .expect("🙈 Failed to create env"); - python } #[tauri::command(async)] -pub fn run_swarm_mode( - handle: tauri::AppHandle, - num_blocks: i32, - model: String, - public_name: String, -) { - let python: String = create_environment(handle); - println!("🚀 Starting the Swarm..."); +pub fn run_swarm(handle: tauri::AppHandle, num_blocks: i32, model: String, public_name: String) { + let petals_path = get_petals_path(handle); + // Get the application data directory + let app_data_dir = tauri::api::path::home_dir().expect("🙈 Failed to get app data directory"); + let app_data_dir = app_data_dir.join(".config/prem"); + let app_data_dir = app_data_dir + .to_str() + .expect("🙈 Failed to convert app data dir path to str"); + + let python = format!("{app_data_dir}/miniconda/envs/prem_app/bin/python"); - let _ = Command::new(&python) - .args(&[ - "-m", - "petals.cli.run_server", - "--num_blocks", + let mut env = HashMap::new(); + env.insert("PREM_APPDIR".to_string(), app_data_dir.to_string()); + env.insert("PREM_PYTHON".to_string(), python.clone()); + + println!("🚀 Starting the Swarm..."); + let _ = Command::new("sh") + .args([ + format!("{petals_path}/run_swarm.sh").as_str(), &num_blocks.to_string(), - "--public_name", &public_name, &model, ]) + .envs(env.clone()) .spawn() .expect("🙈 Failed to run swarm"); } +fn get_petals_path(handle: tauri::AppHandle) -> String { + // Get create env path + let binding = handle + .path_resolver() + .resolve_resource("petals") + .expect("🙈 Failed to find `create_env.sh`"); + let petals_path = binding + .to_str() + .expect("🙈 Failed to convert petals path to str"); + petals_path.to_string() +} + pub fn get_swarm_processes() -> String { // Check if create_env.sh is running let output = Command::new("/usr/bin/pgrep") @@ -144,9 +152,19 @@ pub fn get_swarm_processes() -> String { return "".to_string(); } + let app_data_dir = tauri::api::path::home_dir().expect("🙈 Failed to get app data directory"); + let app_data_dir = app_data_dir.join(".config/prem"); + let app_data_dir = app_data_dir + .to_str() + .expect("🙈 Failed to convert app data dir path to str"); + + let prem_app_env = format!("{app_data_dir}/miniconda/envs/prem_app"); + + let regex = format!("https://github.com/bigscience-workshop/petals|{prem_app_env}.*(petals.cli.run_server|multiprocessing.resource_tracker|from multiprocessing.spawn)"); + // If create_env.sh is not running, get the processes from petals let output = Command::new("/usr/bin/pgrep") - .args(&["-f", "https://github.com/bigscience-workshop/petals|petals.cli.run_server|multiprocessing.resource_tracker|from multiprocessing.spawn"]) + .args(&["-f", ®ex]) .output() .map_err(|e| { println!("🙈 Failed to execute command: {}", e); diff --git a/src/modules/settings/components/SwarmMode.tsx b/src/modules/settings/components/SwarmMode.tsx index bb631795..cc2d3265 100644 --- a/src/modules/settings/components/SwarmMode.tsx +++ b/src/modules/settings/components/SwarmMode.tsx @@ -4,6 +4,7 @@ import { Tooltip } from "react-tooltip"; import Spinner from "shared/components/Spinner"; import { swarmSupported, + createEnvironment, runSwarmMode, checkSwarmModeRunning, stopSwarmMode, @@ -49,6 +50,7 @@ const SwarmMode = () => { try { e.preventDefault(); useSettingStore.getState().setSwarmMode(Swarm.Creating); + await createEnvironment(); const user = await userName(); const publicName = user + "@premAI"; await runSwarmMode(numBlocks, model, publicName); diff --git a/src/shared/helpers/utils.ts b/src/shared/helpers/utils.ts index 14c7558c..1245126d 100644 --- a/src/shared/helpers/utils.ts +++ b/src/shared/helpers/utils.ts @@ -70,12 +70,16 @@ export const stopSwarmMode = async () => { await invoke("stop_swarm_mode"); }; +export const createEnvironment = async () => { + await invoke("create_environment"); +}; + export const runSwarmMode = async (numBlocks: number, model: string, publicName: string) => { const isSwarmMode = await checkSwarmModeRunning(); if (isSwarmMode) { return; } - await invoke("run_swarm_mode", { numBlocks, model, publicName }); + await invoke("run_swarm", { numBlocks, model, publicName }); }; export const isPackaged = () => { From 4b4b494591882b8ebd8e0df8416e1fe92f105fa7 Mon Sep 17 00:00:00 2001 From: nsosio Date: Mon, 30 Oct 2023 20:14:27 +0100 Subject: [PATCH 09/60] fixes --- src-tauri/src/swarm.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index ea73fdc6..647e48ec 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -74,7 +74,6 @@ pub fn is_swarm_mode_running() -> bool { pub fn create_environment(handle: tauri::AppHandle) { println!("🐍 Creating the environment..."); let petals_path = get_petals_path(handle); - let app_data_dir = tauri::api::path::home_dir().expect("🙈 Failed to get app data directory"); let app_data_dir = app_data_dir.join(".config/prem"); let app_data_dir = app_data_dir From 20f1383d1bab726a69d4613dcc8bcedd712a21e4 Mon Sep 17 00:00:00 2001 From: nsosio Date: Fri, 27 Oct 2023 14:27:30 +0200 Subject: [PATCH 10/60] split create_env script --- src-tauri/src/swarm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index 647e48ec..906fdafc 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -192,4 +192,4 @@ pub fn stop_swarm_mode() { }); } println!("🛑 Stopped all the Swarm Processes."); -} +} \ No newline at end of file From 71fb396ca8daa1578277ada98e426dff6cf9b7ea Mon Sep 17 00:00:00 2001 From: nsosio Date: Mon, 30 Oct 2023 20:56:35 +0100 Subject: [PATCH 11/60] fixed lint --- src-tauri/src/swarm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index 906fdafc..647e48ec 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -192,4 +192,4 @@ pub fn stop_swarm_mode() { }); } println!("🛑 Stopped all the Swarm Processes."); -} \ No newline at end of file +} From 69efc474e5ed6ae7fd80e79308ffb95b90541d72 Mon Sep 17 00:00:00 2001 From: nsosio Date: Mon, 30 Oct 2023 22:04:40 +0100 Subject: [PATCH 12/60] updated package --- package-lock.json | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf4114d7..3f8605eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11197,7 +11197,8 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true + "dev": true, + "requires": {} }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -12248,7 +12249,8 @@ "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", - "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==" + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "requires": {} }, "@emotion/utils": { "version": "1.2.1", @@ -13161,7 +13163,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "ajv": { "version": "6.12.6", @@ -14198,7 +14201,8 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", - "dev": true + "dev": true, + "requires": {} }, "eslint-config-react-app": { "version": "7.0.1", @@ -14412,7 +14416,8 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true + "dev": true, + "requires": {} }, "eslint-plugin-testing-library": { "version": "5.11.1", @@ -16676,7 +16681,8 @@ "react-aptor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-aptor/-/react-aptor-2.0.0.tgz", - "integrity": "sha512-YnCayokuhAwmBBP4Oc0bbT2l6ApfsjbY3DEEVUddIKZEBlGl1npzjHHzWnSqWuboSbMZvRqUM01Io9yiIp1wcg==" + "integrity": "sha512-YnCayokuhAwmBBP4Oc0bbT2l6ApfsjbY3DEEVUddIKZEBlGl1npzjHHzWnSqWuboSbMZvRqUM01Io9yiIp1wcg==", + "requires": {} }, "react-copy-to-clipboard": { "version": "5.1.0", @@ -16816,7 +16822,8 @@ "react-simple-code-editor": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.13.1.tgz", - "integrity": "sha512-XYeVwRZwgyKtjNIYcAEgg2FaQcCZwhbarnkJIV20U2wkCU9q/CPFBo8nRXrK4GXUz3AvbqZFsZRrpUTkqqEYyQ==" + "integrity": "sha512-XYeVwRZwgyKtjNIYcAEgg2FaQcCZwhbarnkJIV20U2wkCU9q/CPFBo8nRXrK4GXUz3AvbqZFsZRrpUTkqqEYyQ==", + "requires": {} }, "react-syntax-highlighter": { "version": "15.5.0", @@ -17591,7 +17598,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.2.tgz", "integrity": "sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==", - "dev": true + "dev": true, + "requires": {} }, "tsconfig-paths": { "version": "3.14.2", @@ -17847,17 +17855,20 @@ "use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", - "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==" + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "requires": {} }, "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} }, "usehooks-ts": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.9.1.tgz", - "integrity": "sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==" + "integrity": "sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==", + "requires": {} }, "util-deprecate": { "version": "1.0.2", @@ -18071,7 +18082,8 @@ "yet-another-react-lightbox": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.14.0.tgz", - "integrity": "sha512-UbkxGre2TzzIuRL9bQp6Spbc/9Hgn8JYofs0DJL1cyCc6+ir5I5djJKLs9UpyH61Ak/y81QYF5MA6ve//ggJrw==" + "integrity": "sha512-UbkxGre2TzzIuRL9bQp6Spbc/9Hgn8JYofs0DJL1cyCc6+ir5I5djJKLs9UpyH61Ak/y81QYF5MA6ve//ggJrw==", + "requires": {} }, "yocto-queue": { "version": "0.1.0", From 13007188e9659ab0b1927fe5c9ef199083dbdb40 Mon Sep 17 00:00:00 2001 From: nsosio Date: Wed, 1 Nov 2023 10:55:06 +0100 Subject: [PATCH 13/60] refactoring of env creation and swarm run with config --- src-tauri/src/swarm.rs | 77 +++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index 647e48ec..34bad89f 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -1,6 +1,6 @@ use reqwest::get; use serde::Deserialize; -use std::{collections::HashMap, env, str}; +use std::{collections::HashMap, env, path::PathBuf, str}; use tauri::api::process::Command; #[derive(Deserialize)] @@ -18,6 +18,30 @@ pub fn is_swarm_supported() -> bool { } } +struct Config { + app_data_dir: String, + python: String, +} + +impl Config { + fn new() -> Self { + let mut app_data_dir = + tauri::api::path::home_dir().expect("🙈 Failed to get app data directory"); + app_data_dir.push(".config/prem"); + let app_data_dir = app_data_dir + .to_str() + .expect("🙈 Failed to convert app data dir path to str") + .to_string(); + + let python = format!("{}/miniconda/envs/prem_app/bin/python", app_data_dir); + + Config { + app_data_dir, + python, + } + } +} + #[tauri::command] pub fn get_username() -> String { let output = Command::new("whoami").output(); @@ -71,43 +95,32 @@ pub fn is_swarm_mode_running() -> bool { } #[tauri::command(async)] -pub fn create_environment(handle: tauri::AppHandle) { +pub fn create_environment(handle: tauri::AppHandle) -> HashMap { println!("🐍 Creating the environment..."); let petals_path = get_petals_path(handle); - let app_data_dir = tauri::api::path::home_dir().expect("🙈 Failed to get app data directory"); - let app_data_dir = app_data_dir.join(".config/prem"); - let app_data_dir = app_data_dir - .to_str() - .expect("🙈 Failed to convert app data dir path to str"); - - let python = format!("{app_data_dir}/miniconda/envs/prem_app/bin/python"); + let config = Config::new(); let mut env = HashMap::new(); - env.insert("PREM_APPDIR".to_string(), app_data_dir.to_string()); - env.insert("PREM_PYTHON".to_string(), python.clone()); + env.insert("PREM_APPDIR".to_string(), config.app_data_dir); + env.insert("PREM_PYTHON".to_string(), config.python); - let _ = Command::new("sh") + let output = Command::new("sh") .args([format!("{petals_path}/create_env.sh")]) + .env_clear() .envs(env.clone()) .output() .expect("🙈 Failed to create env"); + println!("{:?}", output.stderr); + env } #[tauri::command(async)] pub fn run_swarm(handle: tauri::AppHandle, num_blocks: i32, model: String, public_name: String) { - let petals_path = get_petals_path(handle); - // Get the application data directory - let app_data_dir = tauri::api::path::home_dir().expect("🙈 Failed to get app data directory"); - let app_data_dir = app_data_dir.join(".config/prem"); - let app_data_dir = app_data_dir - .to_str() - .expect("🙈 Failed to convert app data dir path to str"); - - let python = format!("{app_data_dir}/miniconda/envs/prem_app/bin/python"); + let petals_path = get_petals_path(handle.clone()); + let config = Config::new(); let mut env = HashMap::new(); - env.insert("PREM_APPDIR".to_string(), app_data_dir.to_string()); - env.insert("PREM_PYTHON".to_string(), python.clone()); + env.insert("PREM_PYTHON".to_string(), config.python); println!("🚀 Starting the Swarm..."); let _ = Command::new("sh") @@ -117,13 +130,13 @@ pub fn run_swarm(handle: tauri::AppHandle, num_blocks: i32, model: String, publi &public_name, &model, ]) - .envs(env.clone()) + .env_clear() + .envs(env) .spawn() .expect("🙈 Failed to run swarm"); } fn get_petals_path(handle: tauri::AppHandle) -> String { - // Get create env path let binding = handle .path_resolver() .resolve_resource("petals") @@ -151,13 +164,15 @@ pub fn get_swarm_processes() -> String { return "".to_string(); } - let app_data_dir = tauri::api::path::home_dir().expect("🙈 Failed to get app data directory"); - let app_data_dir = app_data_dir.join(".config/prem"); - let app_data_dir = app_data_dir + let config = Config::new(); + let python_path = PathBuf::from(config.python); + let prem_app_env = python_path + .parent() + .unwrap() + .parent() + .unwrap() .to_str() - .expect("🙈 Failed to convert app data dir path to str"); - - let prem_app_env = format!("{app_data_dir}/miniconda/envs/prem_app"); + .unwrap(); let regex = format!("https://github.com/bigscience-workshop/petals|{prem_app_env}.*(petals.cli.run_server|multiprocessing.resource_tracker|from multiprocessing.spawn)"); From 490534c831f4c64f1afc4a5275953181cb0a45ea Mon Sep 17 00:00:00 2001 From: nsosio Date: Wed, 1 Nov 2023 13:06:29 +0100 Subject: [PATCH 14/60] bugfix in tauri command env --- src-tauri/src/swarm.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index 34bad89f..1d3492a6 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -104,13 +104,11 @@ pub fn create_environment(handle: tauri::AppHandle) -> HashMap { env.insert("PREM_APPDIR".to_string(), config.app_data_dir); env.insert("PREM_PYTHON".to_string(), config.python); - let output = Command::new("sh") + let _ = Command::new("sh") .args([format!("{petals_path}/create_env.sh")]) - .env_clear() .envs(env.clone()) .output() .expect("🙈 Failed to create env"); - println!("{:?}", output.stderr); env } @@ -130,7 +128,6 @@ pub fn run_swarm(handle: tauri::AppHandle, num_blocks: i32, model: String, publi &public_name, &model, ]) - .env_clear() .envs(env) .spawn() .expect("🙈 Failed to run swarm"); From 1a2c5b012c14582d4780d2981d33940465534a84 Mon Sep 17 00:00:00 2001 From: nsosio Date: Wed, 1 Nov 2023 13:41:48 +0100 Subject: [PATCH 15/60] added environment deletion --- src-tauri/src/main.rs | 2 ++ src-tauri/src/swarm.rs | 35 ++++++++++++++++--- src/modules/settings/components/SwarmMode.tsx | 28 +++++++++++++++ src/shared/helpers/utils.ts | 9 +++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index dab7c914..62d33969 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -208,6 +208,8 @@ fn main() { swarm::get_username, swarm::get_petals_models, swarm::create_environment, + swarm::has_environment, + swarm::delete_environment, swarm::run_swarm, swarm::stop_swarm_mode, swarm::is_swarm_mode_running diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index 1d3492a6..f984467e 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -1,6 +1,11 @@ use reqwest::get; use serde::Deserialize; -use std::{collections::HashMap, env, path::PathBuf, str}; +use std::{ + collections::HashMap, + env, + path::{Path, PathBuf}, + str, +}; use tauri::api::process::Command; #[derive(Deserialize)] @@ -95,7 +100,7 @@ pub fn is_swarm_mode_running() -> bool { } #[tauri::command(async)] -pub fn create_environment(handle: tauri::AppHandle) -> HashMap { +pub fn create_environment(handle: tauri::AppHandle) { println!("🐍 Creating the environment..."); let petals_path = get_petals_path(handle); let config = Config::new(); @@ -106,10 +111,32 @@ pub fn create_environment(handle: tauri::AppHandle) -> HashMap { let _ = Command::new("sh") .args([format!("{petals_path}/create_env.sh")]) - .envs(env.clone()) + .envs(env) .output() .expect("🙈 Failed to create env"); - env +} + +#[tauri::command(async)] +pub fn has_environment() -> bool { + let config = Config::new(); + let path = PathBuf::from(config.app_data_dir).join("miniconda"); + path.as_path().exists() +} + +#[tauri::command(async)] +pub fn delete_environment(handle: tauri::AppHandle) { + println!("❌ Deleting the environment..."); + let petals_path = get_petals_path(handle); + let config = Config::new(); + + let mut env = HashMap::new(); + env.insert("PREM_APPDIR".to_string(), config.app_data_dir); + + let _ = Command::new("sh") + .args([format!("{petals_path}/delete_env.sh")]) + .envs(env.clone()) + .output() + .expect("🙈 Failed to delete env"); } #[tauri::command(async)] diff --git a/src/modules/settings/components/SwarmMode.tsx b/src/modules/settings/components/SwarmMode.tsx index cc2d3265..85a982d3 100644 --- a/src/modules/settings/components/SwarmMode.tsx +++ b/src/modules/settings/components/SwarmMode.tsx @@ -1,10 +1,13 @@ /* eslint-disable prettier/prettier */ import { useState, useEffect } from "react"; import { Tooltip } from "react-tooltip"; +import DeleteIconNew from "shared/components/DeleteIconNew"; import Spinner from "shared/components/Spinner"; import { swarmSupported, createEnvironment, + checkHasEnvironment, + deleteEnvironment, runSwarmMode, checkSwarmModeRunning, stopSwarmMode, @@ -21,6 +24,7 @@ import SwarmModeModal from "./SwarmModeModal"; const SwarmMode = () => { const swarmMode = useSettingStore((state) => state.swarmMode); const [isSwarmSupported, setIsSwarmSupported] = useState(false); + const [hasEnvironment, setHasEnvironment] = useState(false); const [numBlocks, setNumBlocks] = useState(3); const [modelOptions, setModelOptions] = useState([]); const [model, setModel] = useState(""); @@ -72,6 +76,19 @@ const SwarmMode = () => { } }; + useEffect(() => { + checkHasEnvironment().then(setHasEnvironment); + }, []); + + const onDeleteClick = async () => { + try { + await deleteEnvironment(); + setHasEnvironment(false); + } catch (error) { + console.error(error); + } + }; + useEffect(() => { async function fetchData() { try { @@ -115,6 +132,17 @@ const SwarmMode = () => { ) : swarmMode === Swarm.Inactive ? ( <> + {hasEnvironment ? ( +
    + + + + {
    Delete Swarm environment
    } +
    +
    + ) : null}
    diff --git a/src/shared/helpers/utils.ts b/src/shared/helpers/utils.ts index 1245126d..4bf6cb67 100644 --- a/src/shared/helpers/utils.ts +++ b/src/shared/helpers/utils.ts @@ -74,6 +74,15 @@ export const createEnvironment = async () => { await invoke("create_environment"); }; +export const deleteEnvironment = async () => { + await invoke("delete_environment"); +}; + +export const checkHasEnvironment = async () => { + const check = await invoke("has_environment"); + return Boolean(check); +}; + export const runSwarmMode = async (numBlocks: number, model: string, publicName: string) => { const isSwarmMode = await checkSwarmModeRunning(); if (isSwarmMode) { From 33d9876cfcfbad5fda0ed472e7884b76201ed4f3 Mon Sep 17 00:00:00 2001 From: nsosio Date: Wed, 1 Nov 2023 16:10:13 +0100 Subject: [PATCH 16/60] improved deletion ui --- src-tauri/src/swarm.rs | 15 ++++++++------- src/modules/settings/components/SwarmMode.tsx | 18 +++++++++--------- src/shared/store/setting.ts | 5 ++++- src/shared/types/index.ts | 8 ++++++++ 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index f984467e..06fe3d5e 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -1,11 +1,6 @@ use reqwest::get; use serde::Deserialize; -use std::{ - collections::HashMap, - env, - path::{Path, PathBuf}, - str, -}; +use std::{collections::HashMap, env, path::PathBuf, str}; use tauri::api::process::Command; #[derive(Deserialize)] @@ -38,7 +33,13 @@ impl Config { .expect("🙈 Failed to convert app data dir path to str") .to_string(); - let python = format!("{}/miniconda/envs/prem_app/bin/python", app_data_dir); + let python = PathBuf::from(format!( + "{}/miniconda/envs/prem_app/bin/python", + app_data_dir + )) + .to_str() + .unwrap_or("python") + .to_string(); Config { app_data_dir, diff --git a/src/modules/settings/components/SwarmMode.tsx b/src/modules/settings/components/SwarmMode.tsx index 85a982d3..00f5d8e3 100644 --- a/src/modules/settings/components/SwarmMode.tsx +++ b/src/modules/settings/components/SwarmMode.tsx @@ -15,7 +15,7 @@ import { userName, } from "shared/helpers/utils"; import useSettingStore from "shared/store/setting"; -import { Swarm } from "shared/types"; +import { EnvironmentDeletion, Swarm } from "shared/types"; import PrimaryButton from "../../../shared/components/PrimaryButton"; @@ -24,7 +24,7 @@ import SwarmModeModal from "./SwarmModeModal"; const SwarmMode = () => { const swarmMode = useSettingStore((state) => state.swarmMode); const [isSwarmSupported, setIsSwarmSupported] = useState(false); - const [hasEnvironment, setHasEnvironment] = useState(false); + const hasEnvironment = useSettingStore((state) => state.environmentDeletion); const [numBlocks, setNumBlocks] = useState(3); const [modelOptions, setModelOptions] = useState([]); const [model, setModel] = useState(""); @@ -55,6 +55,7 @@ const SwarmMode = () => { e.preventDefault(); useSettingStore.getState().setSwarmMode(Swarm.Creating); await createEnvironment(); + useSettingStore.getState().setEnvironmentDeletion(EnvironmentDeletion.Idle); const user = await userName(); const publicName = user + "@premAI"; await runSwarmMode(numBlocks, model, publicName); @@ -76,14 +77,11 @@ const SwarmMode = () => { } }; - useEffect(() => { - checkHasEnvironment().then(setHasEnvironment); - }, []); - const onDeleteClick = async () => { try { + useSettingStore.getState().setEnvironmentDeletion(EnvironmentDeletion.Progress); await deleteEnvironment(); - setHasEnvironment(false); + useSettingStore.getState().setEnvironmentDeletion(EnvironmentDeletion.Completed); } catch (error) { console.error(error); } @@ -132,16 +130,18 @@ const SwarmMode = () => { ) : swarmMode === Swarm.Inactive ? ( <> - {hasEnvironment ? ( + {hasEnvironment === EnvironmentDeletion.Idle ? (
    - {
    Delete Swarm environment
    } + {
    Delete Swarm Environment
    }
    + ) : hasEnvironment === EnvironmentDeletion.Progress ? ( + ) : null}
    diff --git a/src/shared/store/setting.ts b/src/shared/store/setting.ts index d5148202..4e2c2ad4 100644 --- a/src/shared/store/setting.ts +++ b/src/shared/store/setting.ts @@ -1,7 +1,7 @@ import storage from "shared/helpers/custom-storage"; import { isIP, isPackaged, isProxyEnabled } from "shared/helpers/utils"; import type { SettingStore, SwarmInfo } from "shared/types"; -import { Swarm } from "shared/types"; +import { EnvironmentDeletion, Swarm } from "shared/types"; import { create } from "zustand"; import { createJSONStorage, devtools, persist } from "zustand/middleware"; @@ -93,6 +93,9 @@ const useSettingStore = create()( swarmInfo: null, setSwarmInfo: (swarmInfo: SwarmInfo | null) => set(() => ({ swarmInfo }), false, "setSwarmInfo"), + environmentDeletion: EnvironmentDeletion.Completed, + setEnvironmentDeletion: (environmentDeletion: EnvironmentDeletion) => + set(() => ({ environmentDeletion }), false, "setEnvironmentDeletion"), }), { name: "setting", diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 305b0eae..1f40c8f0 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -42,6 +42,12 @@ export enum Swarm { Active = "active", } +export enum EnvironmentDeletion { + Idle = "idle", + Progress = "progress", + Completed = "completed", +} + export type SwarmInfo = { model: string; publicName: string; @@ -115,6 +121,8 @@ export type SettingStore = { setSwarmMode: (mode: Swarm) => void; swarmInfo: SwarmInfo | null; setSwarmInfo: (swarmInfo: SwarmInfo | null) => void; + environmentDeletion: EnvironmentDeletion | null; + setEnvironmentDeletion: (environmentDeletion: EnvironmentDeletion) => void; }; export type HeaderProps = { From 6413cc540a8405fa2e3ab2c63cf667ebce22f824 Mon Sep 17 00:00:00 2001 From: nsosio Date: Wed, 1 Nov 2023 16:35:30 +0100 Subject: [PATCH 17/60] added tooltip on deletion progress --- src-tauri/src/main.rs | 1 - src-tauri/src/swarm.rs | 7 ------- src/modules/settings/components/SwarmMode.tsx | 10 ++++++++-- src/shared/helpers/utils.ts | 5 ----- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 62d33969..ac09503c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -208,7 +208,6 @@ fn main() { swarm::get_username, swarm::get_petals_models, swarm::create_environment, - swarm::has_environment, swarm::delete_environment, swarm::run_swarm, swarm::stop_swarm_mode, diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index 06fe3d5e..f652599c 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -117,13 +117,6 @@ pub fn create_environment(handle: tauri::AppHandle) { .expect("🙈 Failed to create env"); } -#[tauri::command(async)] -pub fn has_environment() -> bool { - let config = Config::new(); - let path = PathBuf::from(config.app_data_dir).join("miniconda"); - path.as_path().exists() -} - #[tauri::command(async)] pub fn delete_environment(handle: tauri::AppHandle) { println!("❌ Deleting the environment..."); diff --git a/src/modules/settings/components/SwarmMode.tsx b/src/modules/settings/components/SwarmMode.tsx index 00f5d8e3..88ac0a24 100644 --- a/src/modules/settings/components/SwarmMode.tsx +++ b/src/modules/settings/components/SwarmMode.tsx @@ -6,7 +6,6 @@ import Spinner from "shared/components/Spinner"; import { swarmSupported, createEnvironment, - checkHasEnvironment, deleteEnvironment, runSwarmMode, checkSwarmModeRunning, @@ -141,7 +140,14 @@ const SwarmMode = () => {
    ) : hasEnvironment === EnvironmentDeletion.Progress ? ( - + <> +
    + +
    + + {
    Deleting Swarm Environment
    } +
    + ) : null}
    diff --git a/src/shared/helpers/utils.ts b/src/shared/helpers/utils.ts index 4bf6cb67..2048f545 100644 --- a/src/shared/helpers/utils.ts +++ b/src/shared/helpers/utils.ts @@ -78,11 +78,6 @@ export const deleteEnvironment = async () => { await invoke("delete_environment"); }; -export const checkHasEnvironment = async () => { - const check = await invoke("has_environment"); - return Boolean(check); -}; - export const runSwarmMode = async (numBlocks: number, model: string, publicName: string) => { const isSwarmMode = await checkSwarmModeRunning(); if (isSwarmMode) { From 6836a33673274a59bc5d8098424bf286f026a0c8 Mon Sep 17 00:00:00 2001 From: Nicola Sosio Date: Wed, 1 Nov 2023 19:27:46 +0100 Subject: [PATCH 18/60] fix Co-authored-by: Casper da Costa-Luis --- src-tauri/petals/run_swarm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/petals/run_swarm.sh b/src-tauri/petals/run_swarm.sh index 7320b7f2..b44d183d 100755 --- a/src-tauri/petals/run_swarm.sh +++ b/src-tauri/petals/run_swarm.sh @@ -7,7 +7,7 @@ # public_name: The public name to use. # model: The model to use. -prem_python="${PREM_PYTHON:-.}" +prem_python="${PREM_PYTHON:-python}" requirements="$(dirname "$0")/requirements.txt" echo "Installing requirements" From 2dec37634ae78bd9ab726091ab1701a45c64918e Mon Sep 17 00:00:00 2001 From: Marco Argentieri <3596602+tiero@users.noreply.github.com> Date: Wed, 1 Nov 2023 19:41:08 +0100 Subject: [PATCH 19/60] Update src/modules/settings/components/SwarmMode.tsx --- src/modules/settings/components/SwarmMode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/settings/components/SwarmMode.tsx b/src/modules/settings/components/SwarmMode.tsx index 88ac0a24..acc869f6 100644 --- a/src/modules/settings/components/SwarmMode.tsx +++ b/src/modules/settings/components/SwarmMode.tsx @@ -136,7 +136,7 @@ const SwarmMode = () => { - {
    Delete Swarm Environment
    } + {
    Reset to default
    }
    ) : hasEnvironment === EnvironmentDeletion.Progress ? ( From fb07450d4b41d93311bbffa6ec412c0afdc4b202 Mon Sep 17 00:00:00 2001 From: nsosio Date: Wed, 1 Nov 2023 20:00:19 +0100 Subject: [PATCH 20/60] removed swarm from user messages --- src/modules/settings/components/SwarmMode.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/settings/components/SwarmMode.tsx b/src/modules/settings/components/SwarmMode.tsx index acc869f6..258103f8 100644 --- a/src/modules/settings/components/SwarmMode.tsx +++ b/src/modules/settings/components/SwarmMode.tsx @@ -145,7 +145,7 @@ const SwarmMode = () => {
    - {
    Deleting Swarm Environment
    } + {
    Deleting Prem Network environment
    }
    ) : null} @@ -214,7 +214,8 @@ const SwarmMode = () => { ) : (
    - Please do not close the app we are configuring the Swarm Environment... + Please do not close the app as we are configuring your environment for the Prem + Network...
    From 58a44b30542140ac17263ebff8627685d8cedfdc Mon Sep 17 00:00:00 2001 From: nsosio Date: Thu, 2 Nov 2023 10:53:01 +0100 Subject: [PATCH 21/60] bugfix of swarm state on restart --- src/App.tsx | 1 + src/shared/store/setting.ts | 10 ++++++++++ src/shared/types/index.ts | 1 + 3 files changed, 12 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index aa6b53d6..1ba71bc5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,7 @@ function App() { useSettingStore.getState().setIsIP(hostIsIP); useSettingStore.getState().setBackendUrl(getBackendUrl()); useSettingStore.getState().removeAllServiceAsDownloading(); + useSettingStore.getState().resetSwarm(); // Download progress if (isDesktopEnv()) { diff --git a/src/shared/store/setting.ts b/src/shared/store/setting.ts index 4e2c2ad4..fa8cb5c8 100644 --- a/src/shared/store/setting.ts +++ b/src/shared/store/setting.ts @@ -96,6 +96,16 @@ const useSettingStore = create()( environmentDeletion: EnvironmentDeletion.Completed, setEnvironmentDeletion: (environmentDeletion: EnvironmentDeletion) => set(() => ({ environmentDeletion }), false, "setEnvironmentDeletion"), + resetSwarm: () => { + set( + () => ({ + swarmInfo: null, + swarmMode: Swarm.Inactive, + }), + false, + "resetSwarm", + ); + }, }), { name: "setting", diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 1f40c8f0..c3ef22f8 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -123,6 +123,7 @@ export type SettingStore = { setSwarmInfo: (swarmInfo: SwarmInfo | null) => void; environmentDeletion: EnvironmentDeletion | null; setEnvironmentDeletion: (environmentDeletion: EnvironmentDeletion) => void; + resetSwarm: () => void; }; export type HeaderProps = { From f6d400da74d855bed5f478d114d32e0029a38e15 Mon Sep 17 00:00:00 2001 From: Janaka-Steph Date: Thu, 2 Nov 2023 13:34:12 +0100 Subject: [PATCH 22/60] Only show available section when tag is clicked (#467) --- src/modules/service/components/Service.tsx | 88 ++++++++++++---------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/src/modules/service/components/Service.tsx b/src/modules/service/components/Service.tsx index 697c4438..a1e14ed9 100644 --- a/src/modules/service/components/Service.tsx +++ b/src/modules/service/components/Service.tsx @@ -29,7 +29,7 @@ const Service = () => { const progresses = useSettingStore((state) => state.serviceDownloadsInProgress); const { mutate: download } = useDownloadServiceStream(); const downloadingServices = useSettingStore.getState().downloadingServices; - const [filter, setFilter] = useState(new Map()); + const [filters, setFilters] = useState(new Map()); useEffect(() => { const _apps = apps?.concat({ @@ -43,10 +43,10 @@ const Service = () => { }, [apps]); const filteredApps = useMemo(() => { - if (filter.size === 0) return appsAugmented; - if (![...filter.values()].includes(true)) return appsAugmented; - return appsAugmented?.filter((app) => filter.get(app.id) as boolean); - }, [appsAugmented, filter]); + if (filters.size === 0) return appsAugmented; + if (![...filters.values()].includes(true)) return appsAugmented; + return appsAugmented?.filter((app) => filters.get(app.id) as boolean); + }, [appsAugmented, filters]); // Resume service download if in progress useEffect(() => { @@ -78,42 +78,50 @@ const Service = () => { }, [Object.keys(progresses).length, isServicesLoading, downloadingServices.length]); const ServicesComponents = useMemo(() => { - return filteredApps?.map((app) => { - const filteredServices = - services?.filter((service) => { - if (app.id === "available") { - return checkIfAccessible(getServiceStatus(service)); - } else { - return service?.interfaces?.includes(app.id); - } - }) ?? []; - return ( -
    -

    - {app.name} -

    -
    - {filteredServices - .sort((a, b) => (checkIfAccessible(getServiceStatus(a)) ? -1 : 1)) - .map((service, index) => ( - - ))} + return filteredApps + .filter((app) => { + if (filters.get("available")) { + return app.id === "available"; + } else { + return app.id !== "available"; + } + }) + .map((app) => { + const filteredServices = + services?.filter((service) => { + if (app.id === "available") { + return checkIfAccessible(getServiceStatus(service)); + } else { + return service?.interfaces?.includes(app.id); + } + }) ?? []; + return ( +
    +

    + {app.name} +

    +
    + {filteredServices + .sort((a, b) => (checkIfAccessible(getServiceStatus(a)) ? -1 : 1)) + .map((service, index) => ( + + ))} - {!isServicesLoading && filteredServices.length === 0 && ( -
    No services found
    - )} - {isServicesLoading &&
    Loading...
    } - {isDeveloperMode() && } + {!isServicesLoading && filteredServices.length === 0 && ( +
    No services found
    + )} + {isServicesLoading &&
    Loading...
    } + {isDeveloperMode() && } +
    -
    - ); - }); + ); + }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ filteredApps?.length, @@ -128,7 +136,7 @@ const Service = () => {

    Dashboard

    {appsAugmented && appsAugmented.length > 0 && ( - + )} {ServicesComponents} From 70993250f41e5fd5eabcf31a2bddccddc7807fd9 Mon Sep 17 00:00:00 2001 From: Janaka-Steph Date: Thu, 2 Nov 2023 17:12:20 +0100 Subject: [PATCH 23/60] Set progress to zero when download is clicked (#469) --- src/controller/dockerController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controller/dockerController.ts b/src/controller/dockerController.ts index 094d3ba6..839730bd 100644 --- a/src/controller/dockerController.ts +++ b/src/controller/dockerController.ts @@ -31,6 +31,7 @@ class DockerController extends AbstractServiceController { afterSuccess?: () => void; }): Promise { console.log(`Downloading service ${serviceId}`); + useSettingStore.getState().setServiceDownloadProgress(serviceId, "docker", 0); await downloadServiceStream( serviceId, (error) => { From 05faeda6d14f12d1d94356cc53605f6920569964 Mon Sep 17 00:00:00 2001 From: Nicola Sosio Date: Thu, 2 Nov 2023 17:47:37 +0100 Subject: [PATCH 24/60] Enhance stop swarm process (#468) * added SIGTERM before SIGKILL on stop swarm processes * improved get processes * removed async --- src-tauri/src/swarm.rs | 72 ++++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index f652599c..8f08e459 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -1,6 +1,13 @@ use reqwest::get; use serde::Deserialize; -use std::{collections::HashMap, env, path::PathBuf, str}; +use std::{ + collections::HashMap, + env, + path::PathBuf, + str, + thread::{self, JoinHandle}, + time::Duration, +}; use tauri::api::process::Command; #[derive(Deserialize)] @@ -88,13 +95,10 @@ pub async fn get_petals_models() -> Result, String> { #[tauri::command] pub fn is_swarm_mode_running() -> bool { - let output_value = get_swarm_processes(); + let processes = get_swarm_processes(); - if !output_value.is_empty() { - println!( - "🏃‍♀️ Processeses running: {}", - output_value.replace("\n", " ") - ); + if processes.len() > 0 { + println!("🏃‍♀️ Processeses running: {:?}", processes); return true; } return false; @@ -165,7 +169,7 @@ fn get_petals_path(handle: tauri::AppHandle) -> String { petals_path.to_string() } -pub fn get_swarm_processes() -> String { +pub fn get_swarm_processes() -> Vec { // Check if create_env.sh is running let output = Command::new("/usr/bin/pgrep") .args(&["-f", "create_env.sh|(mamba|conda).*create.*prem_app"]) @@ -179,7 +183,7 @@ pub fn get_swarm_processes() -> String { // If create_env.sh is running, return an empty string if !output_value.is_empty() { - return "".to_string(); + return vec![]; } let config = Config::new(); @@ -204,25 +208,53 @@ pub fn get_swarm_processes() -> String { }); let output_value = output.unwrap().stdout; - output_value + let processes: Vec = output_value + .replace("\n", " ") + .split(" ") + .collect::>() + .into_iter() + .filter_map(|s| s.parse::().ok()) + .collect(); + processes } #[tauri::command] pub fn stop_swarm_mode() { println!("🛑 Stopping the Swarm..."); - let processes = get_swarm_processes().replace("\n", " "); - println!("🛑 Stopping Processes: {}", processes); - let processes = processes.split(" ").collect::>(); + let processes = get_swarm_processes(); + println!("🛑 Stopping Processes: {:?}", processes); for process in processes { - println!("🛑 Stopping Process: {}", process); let _ = Command::new("kill") - .args(&[process.to_string()]) - .output() - .map_err(|e| { - println!("🙈 Failed to execute command: {}", e); - e - }); + .args(["-s", "SIGTERM", &process.to_string()]) + .spawn() + .expect("🙈 Failed to execute kill command with SIGTERM"); + + let handle: JoinHandle<_> = thread::spawn(move || { + thread::sleep(Duration::from_millis(50)); + match Command::new("ps") + .args(["-p", &process.to_string()]) + .output() + { + Ok(output) => match output.status.code() { + Some(0) => true, + _ => false, + }, + Err(e) => { + eprintln!("Error executing ps command: {}", e); + false + } + } + }); + if handle.join().unwrap() { + let _ = Command::new("kill") + .args(["-s", "SIGKILL", &process.to_string()]) + .output() + .expect("🙈 Failed to execute kill command with SIGKILL"); + println!("🛑 Stopping Process with SIGKILL: {}", process); + } else { + println!("🛑 Stopping Process with SIGTERM: {}", process); + } } println!("🛑 Stopped all the Swarm Processes."); } From 34123c5a37c0f6f2f2db30a7f4e8da7cab1cc308 Mon Sep 17 00:00:00 2001 From: tiero <3596602+tiero@users.noreply.github.com> Date: Thu, 2 Nov 2023 18:19:32 +0100 Subject: [PATCH 25/60] docker-compose.gateway point to latest --- docker-compose.gateway.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker-compose.gateway.yml b/docker-compose.gateway.yml index 1e88cb72..6fedfd79 100644 --- a/docker-compose.gateway.yml +++ b/docker-compose.gateway.yml @@ -19,13 +19,12 @@ services: premd: container_name: premd - image: ghcr.io/premai-io/premd:7af3782b00f6176b34840f608327c051502511ce + image: ghcr.io/premai-io/premd:latest volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - PREM_REGISTRY_URL=https://raw.githubusercontent.com/premAI-io/prem-registry/main/manifests.json - PROXY_ENABLED=True - - DOCKER_NETWORK=prem-app_default labels: - "traefik.enable=true" - "traefik.http.routers.premd.rule=PathPrefix(`/premd`)" From 0ad75a39ea93b3a1c2abf118aeaf3886ced9f4e0 Mon Sep 17 00:00:00 2001 From: tiero <3596602+tiero@users.noreply.github.com> Date: Wed, 1 Nov 2023 01:59:01 +0100 Subject: [PATCH 26/60] use v1 branch of prem registry for the app, not dev --- src-tauri/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ac09503c..c86dcec5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -257,7 +257,7 @@ fn main() { .setup(|app| { tauri::async_runtime::block_on(async move { utils::fetch_services_manifests( - "https://raw.githubusercontent.com/premAI-io/prem-registry/dev/manifests.json", + "https://raw.githubusercontent.com/premAI-io/prem-registry/v1/manifests.json", app.state::>().deref().clone(), ) .await From 501a9b2bd7919b007d6bcb66134337b1e4bcd47a Mon Sep 17 00:00:00 2001 From: Janaka-Steph Date: Thu, 2 Nov 2023 18:51:34 +0100 Subject: [PATCH 27/60] removeServiceAsDownloading only in close/error (#470) events --- src/controller/serviceController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controller/serviceController.ts b/src/controller/serviceController.ts index 6989cd92..6a61ceec 100644 --- a/src/controller/serviceController.ts +++ b/src/controller/serviceController.ts @@ -104,7 +104,6 @@ class ServiceController implements IServiceController { if (serviceType === "docker") { useSettingStore.getState().addServiceAsDownloading(serviceId); await this.dockerController.download({ serviceId, afterSuccess }); - useSettingStore.getState().removeServiceAsDownloading(serviceId); } else if (serviceType === "binary") { useSettingStore.getState().addServiceAsDownloading(serviceId); await this.binariesController.download({ From 9e4cf8e7bb350182fd95e3d54107fb540b896a30 Mon Sep 17 00:00:00 2001 From: Swarnim Arun Date: Fri, 3 Nov 2023 01:19:56 +0530 Subject: [PATCH 28/60] fix: buggy state fetch (#471) --- src-tauri/src/download.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs index e59526a2..c42bfbd9 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/download.rs @@ -1,5 +1,6 @@ use crate::errors::{Context, Result}; use std::collections::HashMap; +use std::sync::Arc; use crate::{err, utils, SharedState}; use futures::StreamExt; @@ -144,7 +145,7 @@ impl Downloader { total_file_size: u64, size_on_disk: u64, ) -> Result<()> { - let state = self.window.state::(); + let state = self.window.state::>(); let mut downloading_files_guard = state.downloading_files.lock().await; if downloading_files_guard.contains(&output_path.as_ref().to_string()) { log::warn!("File already downloading: {}", output_path.as_ref()); From a55b2ea1e8cf5a09419129a692b7f8d4116f3aea Mon Sep 17 00:00:00 2001 From: Swarnim Arun Date: Fri, 3 Nov 2023 16:53:24 +0530 Subject: [PATCH 29/60] add: handle edge cases & tests for macos (#477) --- src-tauri/src/utils.rs | 83 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 4cacfcd2..43a2fd71 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -44,17 +44,90 @@ pub fn get_binary_url(binaries_url: &HashMap>) -> Result< err!("Unsupported OS") } else if is_macos() { if is_x86_64() { - binaries_url.get("x86_64-apple-darwin") + binaries_url.get("x86_64-apple-darwin").cloned().flatten() } else if is_aarch64() { - binaries_url.get("aarch64-apple-darwin") + binaries_url.get("aarch64-apple-darwin").cloned().flatten() } else { err!("Unsupported architecture") } - .or_else(|| binaries_url.get("universal-apple-darwin")) - .with_context(|| "No binary available for platform")? - .clone() + .or_else(|| { + binaries_url + .get("universal-apple-darwin") + .cloned() + .flatten() + }) .with_context(|| "Service not supported on your platform") } else { err!("Unsupported platform") } } + +#[cfg(all(test, target_os = "macos"))] +mod tests { + use super::get_binary_url; + use std::collections::HashMap; + + #[test] + fn binary_url_test_universal_only() { + let url = get_binary_url(&HashMap::from_iter([( + "universal-apple-darwin".to_string(), + Some("randome_url.com".to_string()), + )])) + .unwrap(); + assert_eq!(url, "randome_url.com"); + } + + #[test] + fn binary_url_test_universal_only_option() { + let url = get_binary_url(&HashMap::from_iter([ + ( + "universal-apple-darwin".to_string(), + Some("randome_url.com".to_string()), + ), + ("aarch64-apple-darwin".to_string(), None), + ("x86_64-apple-darwin".to_string(), None), + ])) + .unwrap(); + assert_eq!(url, "randome_url.com"); + } + + #[test] + fn binary_url_test_with_non_universal() { + let url = get_binary_url(&HashMap::from_iter([ + ( + "universal-apple-darwin".to_string(), + Some("randome_url.com//universal".to_string()), + ), + ( + "aarch64-apple-darwin".to_string(), + Some("randome_url.com//not_universal".to_string()), + ), + ( + "x86_64-apple-darwin".to_string(), + Some("randome_url.com//not_universal".to_string()), + ), + ])) + .unwrap(); + assert_eq!(url, "randome_url.com//not_universal"); + } + + #[test] + #[should_panic] + fn binary_url_test_empty() { + let url = get_binary_url(&HashMap::from_iter([])).unwrap(); + assert_eq!(url, "randome_url.com"); + } + + #[test] + fn binary_url_all_none() { + let err = get_binary_url(&HashMap::from_iter([ + ("universal-apple-darwin".to_string(), None), + ("aarch64-apple-darwin".to_string(), None), + ("x86_64-apple-darwin".to_string(), None), + ])); + assert_eq!( + err.err().map(|err| err.to_string()).unwrap_or_default(), + "Service not supported on your platform" + ); + } +} From a29c188cac1b950a19f1edae27f1feb12596d757 Mon Sep 17 00:00:00 2001 From: tiero <3596602+tiero@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:33:42 +0100 Subject: [PATCH 30/60] switch registry dev & v1 based on debug or release Signed-off-by: tiero <3596602+tiero@users.noreply.github.com> --- src-tauri/src/main.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c86dcec5..f5f8f4c7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -256,8 +256,17 @@ fn main() { }) .setup(|app| { tauri::async_runtime::block_on(async move { + // Determine the URL based on whether the app is in debug or release mode + let url = if cfg!(debug_assertions) { + // Debug mode URL + "https://raw.githubusercontent.com/premAI-io/prem-registry/dev/manifests.json" + } else { + // Release mode URL + "https://raw.githubusercontent.com/premAI-io/prem-registry/v1/manifests.json" + }; + utils::fetch_services_manifests( - "https://raw.githubusercontent.com/premAI-io/prem-registry/v1/manifests.json", + url, app.state::>().deref().clone(), ) .await From 4c358805da4b5af210dfb164907d9841887e899e Mon Sep 17 00:00:00 2001 From: Biswaroop Date: Sat, 4 Nov 2023 14:14:19 +0530 Subject: [PATCH 31/60] petals models local support using python env (#435) * added support for swarm mode * stable petals release fixed version * Fix style + add PETALS tag * updated small fix readme * show swarm mode only on macos * added num_blocks parameter; start petals does not work * clean up * bugfix * added hardcoded model selection * added model selection * added username from whomai (with prem-app as fallback) * removed max val for num blocks * build fixed * added public name parameter (default prem-app) * moved petals swarm to sidecar * fix: supports Apple Silicon and little UI tweaks * moved petals sidecar script to scripts; bugfix in SwarmMode * added get_username tauri command * added swarm-mode only for aarch64 * renamed petals binary from petals-aarch64-apple-darwin to petals-universal-apple-darwin * CI: workflow tidy * build_petals: parameterise for arches * moving swarm mode from sidecar to miniconda env * bugfix in swarm command * removed sidecar * added env deletion script * Async run_swarm_mode Co-authored-by: Casper da Costa-Luis * added spinner while creating the environment * replaced wget with user wget * added fix for including path vars * tidy miniconda installation script * more tidy & use curl * use miniforge, more tidy * update delete_env script * fix exe perms * fixed script for conda env; swarm mode for linux and macos; added tooltips; added modal; updated public name with username * minor fixes * moved swarm commands to module * swarm persistent store draft * swarm persistent store bugfix * removed shadow in swarm settings; addded label for swarm settings * added label on swarm environment creation * added clickable link for explorer * Update src/shared/helpers/utils.ts * improve copy * updated conda processes filter Co-authored-by: Casper da Costa-Luis * fmt * add: petals models local support using python env * fix: typo while resolving merge conflict * chore: removed todos * fix: lint * update: changes on create environment due to swarm mode changes * fix: lint * chore: use dev registry --------- Co-authored-by: Filippo Pedrazzini Co-authored-by: Janaka-Steph Co-authored-by: nsosio Co-authored-by: tiero <3596602+tiero@users.noreply.github.com> Co-authored-by: Casper da Costa-Luis --- src-tauri/src/controller_binaries.rs | 21 ++++++++++++++++++++- src-tauri/src/main.rs | 1 + src-tauri/src/swarm.rs | 8 ++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/controller_binaries.rs b/src-tauri/src/controller_binaries.rs index 93934085..a2754715 100644 --- a/src-tauri/src/controller_binaries.rs +++ b/src-tauri/src/controller_binaries.rs @@ -5,7 +5,9 @@ use crate::{ download::Downloader, err, errors::{Context, Result}, - logerr, Service, SharedState, + logerr, + swarm::{create_environment, Config}, + Service, SharedState, }; use std::path::PathBuf; @@ -58,6 +60,7 @@ pub async fn download_service( #[tauri::command(async)] pub async fn start_service( + handle: tauri::AppHandle, service_id: String, state: State<'_, Arc>, app_handle: AppHandle, @@ -97,6 +100,11 @@ pub async fn start_service( ) })? .as_str(); + let is_petals_model = services_guard + .get(&service_id) + .with_context(|| format!("service_id {} doesn't exist in registry", service_id))? + .petals + .unwrap_or_default(); log::info!("serve_command: {}", serve_command); let serve_command_vec: Vec<&str> = serve_command.split_whitespace().collect(); @@ -123,9 +131,20 @@ pub async fn start_service( }) .collect(); log::info!("args: {:?}", args); + + let config = Config::new(); + let mut env_vars = HashMap::new(); + env_vars.insert("PREM_APPDIR".to_string(), config.app_data_dir); + env_vars.insert("PREM_PYTHON".to_string(), config.python); + + if is_petals_model { + create_environment(handle); + } + let child = Command::new(&binary_path) .current_dir(service_dir) .args(args) + .envs(env_vars) .stdout(std::process::Stdio::from( log_file .try_clone() diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f5f8f4c7..cf451d40 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -54,6 +54,7 @@ pub struct Service { running_port: Option, #[serde(rename = "serviceType")] service_type: Option, + petals: Option, version: Option, #[serde(rename = "weightsDirectoryUrl")] weights_directory_url: Option, diff --git a/src-tauri/src/swarm.rs b/src-tauri/src/swarm.rs index 8f08e459..9cb1b184 100644 --- a/src-tauri/src/swarm.rs +++ b/src-tauri/src/swarm.rs @@ -25,13 +25,13 @@ pub fn is_swarm_supported() -> bool { } } -struct Config { - app_data_dir: String, - python: String, +pub struct Config { + pub app_data_dir: String, + pub python: String, } impl Config { - fn new() -> Self { + pub fn new() -> Self { let mut app_data_dir = tauri::api::path::home_dir().expect("🙈 Failed to get app data directory"); app_data_dir.push(".config/prem"); From e17feaecd2d597a9f265ea3e4b490c10adc82f6c Mon Sep 17 00:00:00 2001 From: Marco Argentieri <3596602+tiero@users.noreply.github.com> Date: Sat, 4 Nov 2023 19:44:49 +0100 Subject: [PATCH 32/60] chore: cargo fix (#488) --- src-tauri/src/controller_binaries.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/controller_binaries.rs b/src-tauri/src/controller_binaries.rs index a2754715..efc34f69 100644 --- a/src-tauri/src/controller_binaries.rs +++ b/src-tauri/src/controller_binaries.rs @@ -470,7 +470,7 @@ pub async fn get_system_stats() -> Result> { } #[tauri::command(async)] -pub async fn get_service_stats(service_id: String) -> Result> { +pub async fn get_service_stats(_service_id: String) -> Result> { Ok(HashMap::new()) } #[tauri::command(async)] From de172fda28c4842bec421dabd30d9a4e28a1ef1f Mon Sep 17 00:00:00 2001 From: Marco Argentieri <3596602+tiero@users.noreply.github.com> Date: Sat, 4 Nov 2023 19:45:34 +0100 Subject: [PATCH 33/60] feat: improve service Card (#490) --- src/modules/service/components/Docker.tsx | 7 +++++ src/modules/service/components/Local.tsx | 7 +++++ src/modules/service/components/PeerToPeer.tsx | 7 +++++ src/modules/service/components/Petals.tsx | 7 ----- .../service/components/PremNetwork.tsx | 7 +++++ .../service/components/ServiceCard.tsx | 27 ++++++++++++++----- 6 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 src/modules/service/components/Docker.tsx create mode 100644 src/modules/service/components/Local.tsx create mode 100644 src/modules/service/components/PeerToPeer.tsx delete mode 100644 src/modules/service/components/Petals.tsx create mode 100644 src/modules/service/components/PremNetwork.tsx diff --git a/src/modules/service/components/Docker.tsx b/src/modules/service/components/Docker.tsx new file mode 100644 index 00000000..15bb91d7 --- /dev/null +++ b/src/modules/service/components/Docker.tsx @@ -0,0 +1,7 @@ +import clsx from "clsx"; + +const Docker = ({ className }: React.ButtonHTMLAttributes) => { + return 🐳 DOCKER; +}; + +export default Docker; diff --git a/src/modules/service/components/Local.tsx b/src/modules/service/components/Local.tsx new file mode 100644 index 00000000..8d52e49f --- /dev/null +++ b/src/modules/service/components/Local.tsx @@ -0,0 +1,7 @@ +import clsx from "clsx"; + +const Local = ({ className }: React.ButtonHTMLAttributes) => { + return 🖥️ MY COMPUTER; +}; + +export default Local; diff --git a/src/modules/service/components/PeerToPeer.tsx b/src/modules/service/components/PeerToPeer.tsx new file mode 100644 index 00000000..bcae9b0d --- /dev/null +++ b/src/modules/service/components/PeerToPeer.tsx @@ -0,0 +1,7 @@ +import clsx from "clsx"; + +const PeerToPeer = ({ className }: React.ButtonHTMLAttributes) => { + return 🍐 PEER TO PEER; +}; + +export default PeerToPeer; diff --git a/src/modules/service/components/Petals.tsx b/src/modules/service/components/Petals.tsx deleted file mode 100644 index e99f76ca..00000000 --- a/src/modules/service/components/Petals.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import clsx from "clsx"; - -const Petals = ({ className }: React.ButtonHTMLAttributes) => { - return PETALS; -}; - -export default Petals; diff --git a/src/modules/service/components/PremNetwork.tsx b/src/modules/service/components/PremNetwork.tsx new file mode 100644 index 00000000..7c170288 --- /dev/null +++ b/src/modules/service/components/PremNetwork.tsx @@ -0,0 +1,7 @@ +import clsx from "clsx"; + +const PremNetwork = ({ className }: React.ButtonHTMLAttributes) => { + return 📡 PREM NETWORK; +}; + +export default PremNetwork; diff --git a/src/modules/service/components/ServiceCard.tsx b/src/modules/service/components/ServiceCard.tsx index 8613e344..0127ee81 100644 --- a/src/modules/service/components/ServiceCard.tsx +++ b/src/modules/service/components/ServiceCard.tsx @@ -11,7 +11,10 @@ import useWarningModal from "../hooks/useWarningModal"; import type { ServiceCardProps } from "../types"; import Beta from "./Beta"; -import Petals from "./Petals"; +import Docker from "./Docker"; +import Local from "./Local"; +import PeerToPeer from "./PeerToPeer"; +import PremNetwork from "./PremNetwork"; import ServiceActions from "./ServiceActions"; const ServiceCard = ({ className, service }: ServiceCardProps) => { @@ -56,11 +59,23 @@ const ServiceCard = ({ className, service }: ServiceCardProps) => { isWarningModalOpen={isWarningModalOpen} />
  • -

    - {title} - {service.beta && } - {service.petals && } -

    +
    +

    + {title} + {service.beta && } +

    + {service.petals ? ( +

    + + +

    + ) : ( +

    + + {status === "docker_only" && } +

    + )} +
    ); }; From 97a0b6ed987cc038f59c39dbf17a96c345823fb6 Mon Sep 17 00:00:00 2001 From: Marco Argentieri <3596602+tiero@users.noreply.github.com> Date: Sat, 4 Nov 2023 20:07:14 +0100 Subject: [PATCH 34/60] feat: add Network link (#493) --- src/assets/images/network.png | Bin 0 -> 24809 bytes src/shared/components/DashboardSidebar.tsx | 10 +++++++++- src/shared/components/NetworkIcon.tsx | 7 +++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 src/assets/images/network.png create mode 100644 src/shared/components/NetworkIcon.tsx diff --git a/src/assets/images/network.png b/src/assets/images/network.png new file mode 100644 index 0000000000000000000000000000000000000000..f331088605ccbdd24c180b6841ad7303b1b9045d GIT binary patch literal 24809 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 E2YQ=BqW}N^ literal 0 HcmV?d00001 diff --git a/src/shared/components/DashboardSidebar.tsx b/src/shared/components/DashboardSidebar.tsx index be193c97..1f950839 100644 --- a/src/shared/components/DashboardSidebar.tsx +++ b/src/shared/components/DashboardSidebar.tsx @@ -6,6 +6,7 @@ import DashboardIcon from "./DashboardIcon"; import DocumentationIcon from "./DocumentationIcon"; import NavLinkContainer from "./NavLinkContainer"; import NavLinkItem from "./NavLinkItem"; +import NetworkIcon from "./NetworkIcon"; import SettingIcon from "./SettingIcon"; const DashboardSidebar = () => { @@ -23,10 +24,17 @@ const DashboardSidebar = () => {
    + + } + /> } /> { + return ; +}; + +export default NetworkIcon; From 2bdf101cf9adf8c5a2d0087412083d31300f12f9 Mon Sep 17 00:00:00 2001 From: Marco Argentieri <3596602+tiero@users.noreply.github.com> Date: Mon, 6 Nov 2023 09:12:25 +0000 Subject: [PATCH 35/60] v0.2.0-rc.0 --- latest.json | 18 +++++++++++++----- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 8 ++++---- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/latest.json b/latest.json index 14086336..10abebac 100644 --- a/latest.json +++ b/latest.json @@ -1,11 +1,19 @@ { - "version": "0.1.2", + "version": "0.2.0-rc.0", "notes": "See the assets to download and install this version.", - "pub_date": "2023-10-26T17:02:17.290Z", + "pub_date": "2023-11-06T09:12:24.149Z", "platforms": { - "linux-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTck91dEJzS1kvNnhxSDVuUGVZQ3diN0owNUd1YWd0UldlVmRTWjJsNWFWVHZMbFNxNDE1M2k2Qnd5aEMyNUJJZFlqbFc5VEdQaDlKMzFRejlMc2ZhM25Eb2ozbTJtWXdRPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjk4MzM5MjA0CWZpbGU6cHJlbV8wLjEuMl9hbWQ2NC5BcHBJbWFnZS50YXIuZ3oKWkt6RE5sc3F3Y0dSS1dGdGx2dzR6N3JrVzdkODliQ1l5QVkyTnJSbzZpdkNLSE1Xa08zeXNQaVNTTkpleWk1dkQvdHdLV2wvVmxaalovZHRqSXR3REE9PQo=", - "url": "https://github.com/premAI-io/prem-app/releases/download/v0.1.2/prem_0.1.2_amd64.AppImage.tar.gz" + "darwin-aarch64": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTck91dEJzS1kvNjdTZjQzVDUxajBWSkh0QWhkNU9hL2J5RDJ2NzdabS9ucUNLZGlXeU9zZzlhTytldmtaaXk3OEdyWlJudmJHL2dsWE55ZmNzbGxPT0paM1ZEMEZZcFFjPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjk5MjYxNTgzCWZpbGU6UHJlbS5hcHAudGFyLmd6CkMxYlM4U1B1THNCY0VPS00rc2dCSS9pM002VDFhNG1KbFF5TWsxWEdyaFFxNEJTVlhzcDRsYUZzR2RRNGN4bmxRQWU2dWNxYVU2bjg1Z3pIcmR2MENBPT0K", + "url": "https://github.com/premAI-io/prem-app/releases/download/v0.2.0-rc.0/Prem_universal.app.tar.gz" + }, + "darwin-x86_64": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTck91dEJzS1kvNjdTZjQzVDUxajBWSkh0QWhkNU9hL2J5RDJ2NzdabS9ucUNLZGlXeU9zZzlhTytldmtaaXk3OEdyWlJudmJHL2dsWE55ZmNzbGxPT0paM1ZEMEZZcFFjPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjk5MjYxNTgzCWZpbGU6UHJlbS5hcHAudGFyLmd6CkMxYlM4U1B1THNCY0VPS00rc2dCSS9pM002VDFhNG1KbFF5TWsxWEdyaFFxNEJTVlhzcDRsYUZzR2RRNGN4bmxRQWU2dWNxYVU2bjg1Z3pIcmR2MENBPT0K", + "url": "https://github.com/premAI-io/prem-app/releases/download/v0.2.0-rc.0/Prem_universal.app.tar.gz" + }, + "darwin-universal": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTck91dEJzS1kvNjdTZjQzVDUxajBWSkh0QWhkNU9hL2J5RDJ2NzdabS9ucUNLZGlXeU9zZzlhTytldmtaaXk3OEdyWlJudmJHL2dsWE55ZmNzbGxPT0paM1ZEMEZZcFFjPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjk5MjYxNTgzCWZpbGU6UHJlbS5hcHAudGFyLmd6CkMxYlM4U1B1THNCY0VPS00rc2dCSS9pM002VDFhNG1KbFF5TWsxWEdyaFFxNEJTVlhzcDRsYUZzR2RRNGN4bmxRQWU2dWNxYVU2bjg1Z3pIcmR2MENBPT0K", + "url": "https://github.com/premAI-io/prem-app/releases/download/v0.2.0-rc.0/Prem_universal.app.tar.gz" } } } \ No newline at end of file diff --git a/package.json b/package.json index 266216f0..8a55409f 100644 --- a/package.json +++ b/package.json @@ -74,5 +74,5 @@ "tauri": "tauri" }, "type": "module", - "version": "0.1.2" + "version": "0.2.0-rc.0" } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b7f34784..3f7fe169 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2496,7 +2496,7 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prem-app" -version = "0.1.2" +version = "0.2.0-rc.0" dependencies = [ "chrono", "ctrlc", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 142d29b6..21d58f66 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -46,4 +46,4 @@ license = "" name = "prem-app" repository = "" - version = "0.1.2" + version = "0.2.0-rc.0" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ae5a8a57..b8fb5b2b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "Prem", - "version": "0.1.2" + "version": "0.2.0-rc.0" }, "tauri": { "allowlist": { @@ -27,9 +27,6 @@ }, "bundle": { "active": true, - "resources": [ - "petals/*" - ], "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -38,6 +35,9 @@ "icons/icon.ico" ], "identifier": "io.premai.prem-app", + "resources": [ + "petals/*" + ], "targets": "all" }, "security": { From 39ebc7a3e9616443075752fe6b6499de5540bf9a Mon Sep 17 00:00:00 2001 From: Marco Argentieri <3596602+tiero@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:03:39 +0100 Subject: [PATCH 36/60] Merge v1 into main (#497) * use v1 branch of prem registry * Fix chat history (#439) * Available tag (#433) * Available tag * filter available + rm appId * rm unused var * gha: workflow_dispatch with manual version and branch name * gha: debug * gha: use version as number * v0.1.3 * remove mp3 (#432) * Update to TanStack Query v5 (#443) * fix: binary_url precedence logic (#450) * fix: binary_url precedence logic * fix: lint * Remove isFetching (#460) * fix: match premd routes with slash in docker controller (#455) * fix: gracefully handle process kill (#442) * GHA: split Tauri and Docker in two workflows (#462) * chore: add docker-compose to test app, daemon and gateway * docker-compose.gateway point to stable premd * v0.1.4 * fix: buggy state fetch (#471) * v0.1.5 --------- Co-authored-by: Janaka-Steph Co-authored-by: Biswaroop Co-authored-by: Swarnim Arun --- package-lock.json | 4 ++-- src/modules/service/components/Service.tsx | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3f8605eb..8d8ad00b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prem-app", - "version": "0.1.2", + "version": "0.1.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prem-app", - "version": "0.1.2", + "version": "0.1.4", "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", "@tanstack/react-query": "^5.4.3", diff --git a/src/modules/service/components/Service.tsx b/src/modules/service/components/Service.tsx index a1e14ed9..f0c0804d 100644 --- a/src/modules/service/components/Service.tsx +++ b/src/modules/service/components/Service.tsx @@ -42,6 +42,17 @@ const Service = () => { setAppsAugmented(_apps ?? []); }, [apps]); + useEffect(() => { + const _apps = apps?.concat({ + id: "available", + name: "Available", + playground: false, + documentation: "", + icon: "https://raw.githubusercontent.com/astrit/css.gg/master/icons/svg/smile-mouth-open.svg", + }); + setAppsAugmented(_apps ?? []); + }, [apps]); + const filteredApps = useMemo(() => { if (filters.size === 0) return appsAugmented; if (![...filters.values()].includes(true)) return appsAugmented; From 5f95f38c963f4532764f98ecb74c4dffd43d1094 Mon Sep 17 00:00:00 2001 From: Janaka-Steph Date: Mon, 6 Nov 2023 14:51:36 +0100 Subject: [PATCH 37/60] Skip healthcheck (#483) * Skip healthcheck * Skip healthcheck --- src-tauri/src/controller_binaries.rs | 59 +++++++++++++++++----------- src-tauri/src/main.rs | 2 + 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src-tauri/src/controller_binaries.rs b/src-tauri/src/controller_binaries.rs index efc34f69..4a51ffd0 100644 --- a/src-tauri/src/controller_binaries.rs +++ b/src-tauri/src/controller_binaries.rs @@ -154,31 +154,42 @@ pub async fn start_service( .spawn() .map_err(|e| format!("Failed to spawn child process: {}", e))?; - // Check if the service is running calling /v1 endpoint every 500ms - let interval_duration = Duration::from_millis(500); - let mut interval = interval(interval_duration); - loop { - interval.tick().await; - let base_url = get_base_url(&services_guard[&service_id])?; - let url = format!("{}/v1", base_url); - let client = reqwest::Client::new(); - let res = client.get(&url).send().await; - match res { - Ok(response) => { - // If /v1 is not implemented by the service, it will return 400 Bad Request, consider it as success - if response.status().is_success() - || response.status() == reqwest::StatusCode::BAD_REQUEST - { - let mut running_services_guard = state.running_services.lock().await; - running_services_guard.insert(service_id.clone(), child); - log::info!("Service started: {}", service_id); - break; - } else { - log::error!("Service failed to start: {}", service_id); + let skip_service_check = services_guard + .get(&service_id) + .map(|service| service.skip_health_check.unwrap_or(false)) + .unwrap(); + + if skip_service_check { + let mut running_services_guard = state.running_services.lock().await; + running_services_guard.insert(service_id.clone(), child); + log::info!("Service started: {}", service_id); + } else { + // Check if the service is running calling /v1 endpoint every 500ms + let interval_duration = Duration::from_millis(500); + let mut interval = interval(interval_duration); + loop { + interval.tick().await; + let base_url = get_base_url(&services_guard[&service_id])?; + let url = format!("{}/v1", base_url); + let client = reqwest::Client::new(); + let res = client.get(&url).send().await; + match res { + Ok(response) => { + // If /v1 is not implemented by the service, it will return 400 Bad Request, consider it as success + if response.status().is_success() + || response.status() == reqwest::StatusCode::BAD_REQUEST + { + let mut running_services_guard = state.running_services.lock().await; + running_services_guard.insert(service_id.clone(), child); + log::info!("Service started: {}", service_id); + break; + } else { + log::error!("Service failed to start: {}", service_id); + } + } + Err(e) => { + log::error!("Failed to send request: {}", e); } - } - Err(e) => { - log::error!("Failed to send request: {}", e); } } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index cf451d40..36bcbd72 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -55,6 +55,8 @@ pub struct Service { #[serde(rename = "serviceType")] service_type: Option, petals: Option, + #[serde(rename = "skipHealthCheck")] + skip_health_check: Option, version: Option, #[serde(rename = "weightsDirectoryUrl")] weights_directory_url: Option, From 45a152ddec3fb9f6ccd3e038eb3682da38d7ec0d Mon Sep 17 00:00:00 2001 From: Janaka-Steph Date: Mon, 6 Nov 2023 15:05:57 +0100 Subject: [PATCH 38/60] Autosize textarea (#486) --- src/assets/css/prem-chat.css | 30 ++++++++---- src/modules/prem-chat/components/InputBox.tsx | 29 ------------ .../components/PremChatContainer.tsx | 47 ++++++++++++------- src/shared/hooks/useAutosizeTextarea.ts | 18 +++++++ tailwind.config.js | 3 ++ 5 files changed, 71 insertions(+), 56 deletions(-) delete mode 100644 src/modules/prem-chat/components/InputBox.tsx create mode 100644 src/shared/hooks/useAutosizeTextarea.ts diff --git a/src/assets/css/prem-chat.css b/src/assets/css/prem-chat.css index 9c0969b3..9cb7e5ae 100644 --- a/src/assets/css/prem-chat.css +++ b/src/assets/css/prem-chat.css @@ -35,25 +35,37 @@ @apply bg-transparent cursor-pointer border border-grey-400 text-grey-200 w-full z-10 px-4 py-[6px] appearance-none; } -.prem-chat-input input { - @apply bg-grey-700 text-white md:text-sm text-[13px] outline-none rounded-[29px] h-[57px] mt-[14px] py-2 md:pl-[33px] md:pr-16 pr-12 pl-4 w-full; +.autosize-textarea { + @apply bg-grey-700 text-white leading-6 text-sm outline-none rounded-[29px] h-14 mt-[14px] py-2 md:pl-[33px] md:pr-16 pr-12 pl-4 w-full min-h-14 max-h-40; } -.prem-chat-input input:-webkit-autofill, -.prem-chat-input input:-webkit-autofill:hover, -.prem-chat-input input:-webkit-autofill:active, -.prem-chat-input input:-webkit-autofill:focus { +.autosize-textarea:empty { + line-height: 38px; +} + +.autosize-textarea::placeholder { + line-height: 38px; +} + +.autosize-textarea:-webkit-autofill, +.autosize-textarea:-webkit-autofill:hover, +.autosize-textarea:-webkit-autofill:active, +.autosize-textarea:-webkit-autofill:focus { -webkit-text-fill-color: #302f32; -webkit-box-shadow: 0 0 0 1000px #302f32 inset; transition: background-color 5000s ease-in-out 0s; } -.prem-chat-input img { +.autosize-textarea-container { + @apply relative; +} + +.autosize-textarea-container img { @apply bg-primary-light p-2 w-[35px] h-[35px] rounded-full stroke-white; } -.prem-chat-input button { - @apply absolute md:right-[21px] right-[12px] top-[25px]; +.autosize-textarea-container button { + @apply absolute md:right-[21px] right-[12px] bottom-4; } .prem-chat-bottom { diff --git a/src/modules/prem-chat/components/InputBox.tsx b/src/modules/prem-chat/components/InputBox.tsx deleted file mode 100644 index e6dc59bc..00000000 --- a/src/modules/prem-chat/components/InputBox.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import Send from "assets/images/send.svg"; -import type { ForwardedRef } from "react"; -import { forwardRef } from "react"; - -import type { InputBoxProps } from "../types"; - -const InputBox = forwardRef((props: InputBoxProps, ref: ForwardedRef) => { - const { question, setQuestion, disabled, placeholder } = props; - return ( -
    - setQuestion(e.target.value)} - disabled={disabled} - placeholder={placeholder} - ref={ref} - autoFocus - /> - -
    - ); -}); - -export default InputBox; diff --git a/src/modules/prem-chat/components/PremChatContainer.tsx b/src/modules/prem-chat/components/PremChatContainer.tsx index 797d5813..9a6ea49e 100644 --- a/src/modules/prem-chat/components/PremChatContainer.tsx +++ b/src/modules/prem-chat/components/PremChatContainer.tsx @@ -1,3 +1,4 @@ +import Send from "assets/images/send.svg"; import clsx from "clsx"; import { useEffect, useRef, useState } from "react"; import BotReply from "shared/components/BotReply"; @@ -5,10 +6,10 @@ import UserReply from "shared/components/UserReply"; import usePremChatStream from "shared/hooks/usePremChatStream"; import { useMediaQuery, useWindowSize } from "usehooks-ts"; +import useAutosizeTextArea from "../../../shared/hooks/useAutosizeTextarea"; import type { Message, PremChatContainerProps } from "../types"; import Header from "./Header"; -import InputBox from "./InputBox"; import PremChatSidebar from "./PremChatSidebar"; import RegenerateButton from "./RegenerateButton"; import RightSidebar from "./RightSidebar"; @@ -23,7 +24,7 @@ const PremChatContainer = ({ const [rightSidebar, setRightSidebar] = useState(false); const [hamburgerMenuOpen, setHamburgerMenu] = useState(true); const chatMessageListRef = useRef(null); - const inputRef = useRef(null); + const textAreaRef = useRef(null); const { height } = useWindowSize(); const responsiveMatches = useMediaQuery("(min-width: 768px)"); @@ -40,6 +41,8 @@ const PremChatContainer = ({ abort, } = usePremChatStream(serviceId, serviceType, chatId || null); + useAutosizeTextArea(textAreaRef.current, question); + useEffect(() => { if (chatMessageListRef.current) { chatMessageListRef.current.scrollTop = chatMessageListRef.current.scrollHeight; @@ -47,8 +50,8 @@ const PremChatContainer = ({ }, [chatMessages]); useEffect(() => { - if (!isLoading && inputRef.current) { - inputRef.current.focus(); + if (!isLoading && textAreaRef.current) { + textAreaRef.current.focus(); } }, [isLoading]); @@ -103,20 +106,28 @@ const PremChatContainer = ({
    )} - - + +
    +