Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[desktop] Registry support #459

Merged
merged 22 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


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

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

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
sys-info = "0.9.1"
sysinfo = "0.29.10"
thiserror = "1.0.49"
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }

[dependencies.futures]
default-features = false
Expand Down
145 changes: 143 additions & 2 deletions src-tauri/src/controller_binaries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
errors::{Context, Result},
logerr,
swarm::{create_environment, Config},
Service, SharedState,
utils, Registry, Service, SharedState,
};

use std::path::PathBuf;
Expand All @@ -20,6 +20,7 @@ use sys_info::mem_info;
use sysinfo::{Pid, ProcessExt, ProcessRefreshKind, RefreshKind, SystemExt};

use tauri::{AppHandle, Runtime, State, Window};
use tauri_plugin_store::StoreBuilder;

use tokio::process::{Child, Command};
use tokio::time::interval;
Expand Down Expand Up @@ -470,7 +471,7 @@ pub async fn get_system_stats() -> Result<HashMap<String, String>> {
}

#[tauri::command(async)]
pub async fn get_service_stats(service_id: String) -> Result<HashMap<String, String>> {
pub async fn get_service_stats(_service_id: String) -> Result<HashMap<String, String>> {
Ok(HashMap::new())
}
#[tauri::command(async)]
Expand All @@ -484,3 +485,143 @@ pub async fn add_service(service: Service, state: State<'_, Arc<SharedState>>) -
services_guard.insert(service.get_id()?, service);
Ok(())
}

#[tauri::command(async)]
pub async fn add_registry(
registry: Registry,
app_handle: AppHandle,
state: State<'_, Arc<SharedState>>,
) -> Result<()> {
let store_path = app_handle
.path_resolver()
.app_data_dir()
.with_context(|| "Failed to resolve app data dir")?
.join("store.json");
let mut store = StoreBuilder::new(app_handle, store_path).build();
store.load().with_context(|| "Failed to load store")?;
if let Some(registries) = store.get("registries").cloned() {
match serde_json::from_value::<Vec<Registry>>(registries) {
Ok(mut registries) => {
registries.push(registry);
store
.insert(
"registries".to_string(),
serde_json::to_value(&registries).unwrap(),
)
.with_context(|| "Failed to insert into store")?;
utils::fetch_all_services_manifests(&registries, &state)
.await
.expect("failed to fetch services")
}
Err(e) => println!("Error unwrapping registries: {:?}", e),
}
} else {
let new_registry = vec![registry.clone()];
store
.insert(
"registries".to_string(),
serde_json::to_value(&new_registry).unwrap(),
tiero marked this conversation as resolved.
Show resolved Hide resolved
)
.with_context(|| "Failed to insert into store")?;
utils::fetch_all_services_manifests(&new_registry, &state)
.await
.expect("failed to fetch services")
}
store.save().expect("failed to save store");
Ok(())
}

#[tauri::command(async)]
pub async fn delete_registry(
registry: Registry,
app_handle: AppHandle,
state: State<'_, Arc<SharedState>>,
) -> Result<()> {
let store_path = app_handle
.path_resolver()
.app_data_dir()
.with_context(|| "Failed to resolve app data dir")?
.join("store.json");
let mut store = StoreBuilder::new(app_handle, store_path).build();
store.load().with_context(|| "Failed to load store")?;
if let Some(registries) = store.get("registries").cloned() {
let mut registries = serde_json::from_value::<Vec<Registry>>(registries)
.with_context(|| "Failed to deserialize")?;
registries.retain(|r| r.url != registry.url);
store
.insert(
"registries".to_string(),
serde_json::to_value(registries).with_context(|| "Failed to serialize")?,
)
.with_context(|| "Failed to insert into store")?;
store.save().with_context(|| "Failed to save store")?;

// Reset services state and refetch all registries
let mut services_guard = state.services.lock().await;
services_guard.clear();
drop(services_guard);
if let Some(registries) = store.get("registries").cloned() {
match serde_json::from_value::<Vec<Registry>>(registries) {
Ok(registries) => utils::fetch_all_services_manifests(&registries, &state)
.await
.with_context(|| "Failed to fetch services")?,
Err(e) => log::error!("Error unwrapping registries: {:?}", e),
}
} else {
println!("No registries found");
}
}
Ok(())
}

#[tauri::command(async)]
pub async fn fetch_registries(app_handle: AppHandle) -> Result<Vec<Registry>> {
let store_path = app_handle
.path_resolver()
.app_data_dir()
.with_context(|| "Failed to resolve app data dir")?
.join("store.json");
let mut store = StoreBuilder::new(app_handle, store_path).build();
match store.load() {
Ok(_) => {
if let Some(registries) = store.get("registries").cloned() {
match serde_json::from_value::<Vec<Registry>>(registries) {
Ok(registries) => Ok(registries),
Err(e) => {
log::error!("Error unwrapping registries: {:?}", e);
Ok(Vec::new())
}
}
} else {
log::error!("No registries found");
Ok(Vec::new())
}
}
Err(e) => {
log::error!("Error loading store: {:?}", e);
Ok(Vec::new())
}
}
}

#[tauri::command(async)]
pub async fn reset_default_registry(
app_handle: AppHandle,
state: State<'_, Arc<SharedState>>,
) -> Result<()> {
let store_path = app_handle
.path_resolver()
.app_data_dir()
.with_context(|| "Failed to resolve app data dir")?
.join("store.json");
let mut store = StoreBuilder::new(app_handle.clone(), store_path).build();
store.load().with_context(|| "Failed to load store")?;
store
.delete("registries")
.with_context(|| "Failed to delete registries")?;
store.save().with_context(|| "Failed to save store")?;
add_registry(Registry::default(), app_handle.clone(), state)
.await
.with_context(|| "Failed to add default registry")?;
Ok(())
}
81 changes: 66 additions & 15 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use tauri::{
AboutMetadata, CustomMenuItem, Manager, Menu, MenuItem, RunEvent, Submenu, SystemTray,
SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, WindowEvent,
};
use tauri_plugin_store::StoreBuilder;
use tokio::process::Child;
use tokio::sync::Mutex;

Expand All @@ -28,6 +29,32 @@ pub struct SharedState {
services: Mutex<HashMap<String, Service>>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Registry {
url: String,
}

impl Default for Registry {
fn default() -> Self {
// 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"
};
Registry {
url: url.to_string(),
}
}
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Store {
registries: Vec<Registry>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Service {
// Static state from registry manifest
Expand Down Expand Up @@ -191,6 +218,7 @@ fn main() {

let app = tauri::Builder::default()
.plugin(sentry_tauri::plugin())
.plugin(tauri_plugin_store::Builder::default().build())
.manage(state.clone())
.invoke_handler(tauri::generate_handler![
controller_binaries::start_service,
Expand All @@ -205,6 +233,10 @@ fn main() {
controller_binaries::get_service_stats,
controller_binaries::get_gpu_stats,
controller_binaries::add_service,
controller_binaries::add_registry,
controller_binaries::delete_registry,
controller_binaries::fetch_registries,
controller_binaries::reset_default_registry,
swarm::is_swarm_supported,
swarm::get_username,
swarm::get_petals_models,
Expand Down Expand Up @@ -257,21 +289,40 @@ 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(
url,
app.state::<Arc<SharedState>>().deref().clone(),
)
.await
.expect("Failed to fetch and save services manifests");
//Create a store with default registry if doesn't exist
let store_path = app
.path_resolver()
.app_data_dir()
.expect("failed to resolve app data dir")
.join("store.json");
if !store_path.exists() {
Janaka-Steph marked this conversation as resolved.
Show resolved Hide resolved
let mut registries: Vec<Registry> = Vec::new();
registries.push(Registry::default());
let mut default_store = HashMap::new();
default_store.insert(
"registries".to_string(),
serde_json::to_value(registries).unwrap(),
);
let store = StoreBuilder::new(app.handle(), store_path.clone())
.defaults(default_store)
.build();
store.save().expect("failed to save store");
log::info!("Store created");
}
// Fetch all registries
let mut store = StoreBuilder::new(app.handle(), store_path.clone()).build();
store.load().expect("Failed to load store");
if let Some(registries) = store.get("registries").cloned() {
match serde_json::from_value::<Vec<Registry>>(registries) {
Ok(registries) => utils::fetch_all_services_manifests(
&registries,
&app.state::<Arc<SharedState>>().clone(),
)
.await
.expect("failed to fetch services"),
Err(e) => println!("Error unwrapping registries: {:?}", e),
}
}
});
Ok(())
})
Expand Down
37 changes: 28 additions & 9 deletions src-tauri/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
use crate::errors::{Context, Result};
use crate::{err, Service, SharedState};
use crate::{err, Registry, Service, SharedState};
use futures::future;
use reqwest::get;
use std::collections::HashMap;
use std::sync::Arc;

pub async fn fetch_services_manifests(url: &str, state: Arc<SharedState>) -> Result<()> {
pub async fn fetch_all_services_manifests(
registries: &Vec<Registry>,
Janaka-Steph marked this conversation as resolved.
Show resolved Hide resolved
state: &Arc<SharedState>,
) -> Result<()> {
let mut handlers = vec![];
tiero marked this conversation as resolved.
Show resolved Hide resolved
for registry in registries {
let handler = async move {
if let Err(err) = fetch_services_manifests(registry.url.as_str(), state).await {
println!(
"Failed to fetch {} and save services manifests: {}",
registry.url, err
);
}
};
handlers.push(handler);
}
future::join_all(handlers).await;
Ok(())
}

async fn fetch_services_manifests(url: &str, state: &Arc<SharedState>) -> Result<()> {
let response = get(url)
.await
.with_context(|| format!("Couldn't fetch the manifest from {url:?}"))?;
Expand All @@ -13,13 +34,11 @@ pub async fn fetch_services_manifests(url: &str, state: Arc<SharedState>) -> Res
.await
.with_context(|| "Failed to parse response to list of services")?;
let mut services_guard = state.services.lock().await;
// 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();
for service in services {
if !services_guard.contains_key(&service.id.clone().unwrap_or_default()) {
services_guard.insert(service.get_id()?, service);
}
}
tiero marked this conversation as resolved.
Show resolved Hide resolved
Ok(())
}

Expand Down
5 changes: 5 additions & 0 deletions src/controller/abstractServiceController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Service } from "../modules/service/types";
import type { Registry } from "../modules/settings/types";
import type { Interface } from "../shared/helpers/interfaces";

import type { DownloadArgs } from "./serviceController";
Expand All @@ -23,6 +24,10 @@ abstract class AbstractServiceController {
abstract getGPUStats(): Promise<Record<string, string>>;
abstract getInterfaces(): Promise<Interface[]>;
abstract addService(service: Service): Promise<void>;
abstract addRegistry(registry: Registry): Promise<void>;
abstract deleteRegistry(registry: Registry): Promise<void>;
abstract fetchRegistries(): Promise<Registry[]>;
abstract resetDefaultRegistry(): Promise<void>;
}

export default AbstractServiceController;
Loading