Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion Cargo.lock

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

6 changes: 3 additions & 3 deletions interface/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "lifelog_interface_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

Expand Down Expand Up @@ -43,6 +40,9 @@ dirs = "5.0.1"
base64 = "0.21.0"
async-trait = "0.1"
reqwest = { version = "0.11", features = ["json", "multipart"] }
http = "0.2"
hyper = "0.14"
lazy_static = "1.4.0"

# Cross-platform dependencies
toml = "0.5"
Expand Down
25 changes: 15 additions & 10 deletions interface/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@ use std::error::Error;
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn Error>> {
// Ensure this runs before the tauri_build::build()
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

tonic_build::configure()
.build_client(true) // We need the client code
.build_server(false) // We don't need server code in the interface
.file_descriptor_set_path(out_dir.join("lifelog_descriptor.bin")) // Store the descriptor set
.compile_well_known_types(true) // Generate code for well-known types so serde derive can be applied
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") // Add serde derive attributes to generated message types
.build_client(false)
.build_server(false)
.file_descriptor_set_path(out_dir.join("lifelog_descriptor.bin"))
.compile_well_known_types(true)
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
.client_attribute("lifelog.LifelogServerService", "#[derive(Debug)]")
.client_attribute("lifelog.CollectorService", "#[derive(Debug)]")
.client_attribute("lifelog.LifelogServerServiceClient", "#[allow(dead_code)]")
.client_attribute("lifelog.CollectorServiceClient", "#[allow(dead_code)]")
.client_attribute(".", r#"#[allow(unused_qualifications)]"#)
.extern_path(".google.protobuf", "::prost_types")
.field_attribute("timestamp", "#[serde(with = \"crate::timestamp_serde\")]")
.compile(
&[
"../../proto/lifelog.proto", // Path relative to build.rs
"../../proto/lifelog_types.proto", // Path relative to build.rs
"../../proto/lifelog.proto",
"../../proto/lifelog_types.proto",
],
&["../../proto/"], // Include path for imports within protos
&["../../proto/"],
)?;

// Default Tauri build steps
tauri_build::build();

Ok(())
Expand Down
241 changes: 241 additions & 0 deletions interface/src-tauri/src/grpc_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
use tokio::time::timeout;
use tonic::transport::{Channel, Endpoint};
use serde_json::Value;
use crate::lifelog;
use crate::lifelog::lifelog_server_service_client::LifelogServerServiceClient;

pub const GRPC_SERVER_ADDRESS: &str = "http://127.0.0.1:7182";

const CACHE_TIMEOUT_SECS: u64 = 30;

pub struct ConfigCache {
last_updated: Instant,
configs: std::collections::HashMap<String, Value>,
}

pub struct GrpcClient {
channel: Channel,
client: LifelogServerServiceClient<Channel>,
cache: Arc<Mutex<Option<ConfigCache>>>,
}

impl GrpcClient {
pub async fn new() -> Result<Self, String> {
let endpoint = Endpoint::from_shared(GRPC_SERVER_ADDRESS.to_string())
.map_err(|e| format!("Invalid endpoint URL: {}", e))?;

let connect_result = timeout(
Duration::from_secs(2),
endpoint.connect()
).await;

let channel = match connect_result {
Ok(result) => result.map_err(|e| format!("Failed to connect: {}", e))?,
Err(_) => return Err("Connection timed out".to_string()),
};

let client = LifelogServerServiceClient::new(channel.clone());
let cache = Arc::new(Mutex::new(None));

Ok(Self {
channel,
client,
cache,
})
}

pub async fn get_config(&self, component_name: &str) -> Result<Value, String> {
// Check cache first
{
let cache_guard = self.cache.lock().await;
if let Some(ref cache_data) = *cache_guard {
// Cache valid for 30 seconds
if cache_data.last_updated.elapsed() < Duration::from_secs(CACHE_TIMEOUT_SECS) {
if let Some(config) = cache_data.configs.get(component_name) {
println!("Using cached config for {}", component_name);
return Ok(config.clone());
}
}
}
}

// No valid cache, get from server
let request = tonic::Request::new(lifelog::GetSystemConfigRequest {});

let response_result = timeout(
Duration::from_secs(5),
self.client.clone().get_config(request)
).await;

let response = match response_result {
Ok(result) => result.map_err(|e| format!("gRPC GetConfig error: {}", e))?.into_inner(),
Err(_) => return Err("gRPC call timed out".to_string()),
};

let collector_config = response.config.ok_or_else(|| "No config returned from server".to_string())?
.collector.ok_or_else(|| "No collector config returned from server".to_string())?;

let mut new_cache = std::collections::HashMap::new();

if let Some(screen_config) = &collector_config.screen {
if let Ok(value) = serde_json::to_value(screen_config) {
new_cache.insert("screen".to_string(), value.clone());
if component_name == "screen" {
self.update_cache(new_cache).await;
return Ok(value);
}
}
}

if let Some(camera_config) = &collector_config.camera {
if let Ok(value) = serde_json::to_value(camera_config) {
new_cache.insert("camera".to_string(), value.clone());
if component_name == "camera" {
self.update_cache(new_cache).await;
return Ok(value);
}
}
}

if let Some(microphone_config) = &collector_config.microphone {
if let Ok(value) = serde_json::to_value(microphone_config) {
new_cache.insert("microphone".to_string(), value.clone());
if component_name == "microphone" {
self.update_cache(new_cache).await;
return Ok(value);
}
}
}

if let Some(processes_config) = &collector_config.processes {
if let Ok(value) = serde_json::to_value(processes_config) {
new_cache.insert("processes".to_string(), value.clone());
if component_name == "processes" {
self.update_cache(new_cache).await;
return Ok(value);
}
}
}

// Update cache even if requested component wasn't found
self.update_cache(new_cache).await;

Err(format!("Component {} not found in server response", component_name))
}

pub async fn set_config(&self, component_name: &str, config_value: &Value) -> Result<(), String> {
// First get current config
let request = tonic::Request::new(lifelog::GetSystemConfigRequest {});

let response_result = timeout(
Duration::from_secs(5),
self.client.clone().get_config(request)
).await;

let get_response = match response_result {
Ok(result) => result.map_err(|e| format!("gRPC GetConfig error: {}", e))?.into_inner(),
Err(_) => return Err("gRPC call timed out".to_string()),
};

let mut system_config = get_response.config
.ok_or_else(|| "No config returned from server".to_string())?;

if system_config.collector.is_none() {
return Err("No collector config in system config".to_string());
}

let mut collector_config = system_config.collector.unwrap();

match component_name.to_lowercase().as_str() {
"screen" => {
let screen_config: lifelog::ScreenConfig = serde_json::from_value(config_value.clone())
.map_err(|e| format!("Failed to deserialize screen config: {}", e))?;
collector_config.screen = Some(screen_config);
},
"camera" => {
let camera_config: lifelog::CameraConfig = serde_json::from_value(config_value.clone())
.map_err(|e| format!("Failed to deserialize camera config: {}", e))?;
collector_config.camera = Some(camera_config);
},
"microphone" => {
let microphone_config: lifelog::MicrophoneConfig = serde_json::from_value(config_value.clone())
.map_err(|e| format!("Failed to deserialize microphone config: {}", e))?;
collector_config.microphone = Some(microphone_config);
},
"processes" => {
let processes_config: lifelog::ProcessesConfig = serde_json::from_value(config_value.clone())
.map_err(|e| format!("Failed to deserialize processes config: {}", e))?;
collector_config.processes = Some(processes_config);
},
_ => return Err(format!("Unsupported component: {}", component_name)),
};

system_config.collector = Some(collector_config);

let set_request = tonic::Request::new(lifelog::SetSystemConfigRequest {
config: Some(system_config),
});

let set_response_result = timeout(
Duration::from_secs(5),
self.client.clone().set_config(set_request)
).await;

let set_response = match set_response_result {
Ok(result) => result.map_err(|e| format!("gRPC SetConfig error: {}", e))?.into_inner(),
Err(_) => return Err("gRPC set_config call timed out".to_string()),
};

if !set_response.success {
return Err("Server reported failure in setting config".to_string());
}

self.invalidate_cache().await;

Ok(())
}

async fn update_cache(&self, new_cache: std::collections::HashMap<String, Value>) {
let mut cache_guard = self.cache.lock().await;
*cache_guard = Some(ConfigCache {
last_updated: Instant::now(),
configs: new_cache,
});
}

pub async fn invalidate_cache(&self) {
let mut cache_guard = self.cache.lock().await;
*cache_guard = None;
}
}

lazy_static::lazy_static! {
pub static ref GRPC_CLIENT: tokio::sync::Mutex<Option<Arc<GrpcClient>>> = tokio::sync::Mutex::new(None);
}

pub async fn init_grpc_client() -> Result<(), String> {
let mut client_guard = GRPC_CLIENT.lock().await;
if client_guard.is_none() {
match GrpcClient::new().await {
Ok(client) => {
*client_guard = Some(Arc::new(client));
Ok(())
},
Err(e) => Err(format!("Failed to initialize gRPC client: {}", e)),
}
} else {
Ok(())
}
}

pub async fn get_grpc_client() -> Result<Arc<GrpcClient>, String> {
let client_guard = GRPC_CLIENT.lock().await;
if let Some(client) = &*client_guard {
Ok(client.clone())
} else {
Err("gRPC client not initialized".to_string())
}
}
Loading