use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use kaish_kernel::ast::Value;
use kaish_kernel::interpreter::ExecResult;
use kaish_kernel::vfs::Filesystem;
use kaish_kernel::{Kernel, KernelConfig};
use crate::traits::{ClientError, ClientResult, KernelClient};
fn generate_blob_id() -> String {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let count = COUNTER.fetch_add(1, Ordering::SeqCst);
format!("{:x}-{:x}", timestamp, count)
}
pub struct EmbeddedClient {
kernel: Arc<Kernel>,
}
impl EmbeddedClient {
pub fn new(kernel: Kernel) -> Self {
Self {
kernel: Arc::new(kernel),
}
}
pub fn transient() -> ClientResult<Self> {
let kernel = Kernel::new(KernelConfig::transient())
.map_err(ClientError::Other)?;
Ok(Self::new(kernel))
}
pub fn with_defaults() -> ClientResult<Self> {
let kernel = Kernel::new(KernelConfig::default())
.map_err(ClientError::Other)?;
Ok(Self::new(kernel))
}
pub fn kernel(&self) -> &Kernel {
&self.kernel
}
pub async fn execute_streaming(
&self,
input: &str,
on_output: &mut dyn FnMut(&ExecResult),
) -> ClientResult<ExecResult> {
self.kernel
.execute_streaming(input, on_output)
.await
.map_err(|e| ClientError::Execution(e.to_string()))
}
}
#[async_trait(?Send)]
impl KernelClient for EmbeddedClient {
async fn execute(&self, input: &str) -> ClientResult<ExecResult> {
self.kernel
.execute(input)
.await
.map_err(|e| ClientError::Execution(e.to_string()))
}
async fn get_var(&self, name: &str) -> ClientResult<Option<Value>> {
Ok(self.kernel.get_var(name).await)
}
async fn set_var(&self, name: &str, value: Value) -> ClientResult<()> {
self.kernel.set_var(name, value).await;
Ok(())
}
async fn list_vars(&self) -> ClientResult<Vec<(String, Value)>> {
Ok(self.kernel.list_vars().await)
}
async fn cwd(&self) -> ClientResult<String> {
Ok(self.kernel.cwd().await.to_string_lossy().to_string())
}
async fn set_cwd(&self, path: &str) -> ClientResult<()> {
self.kernel.set_cwd(PathBuf::from(path)).await;
Ok(())
}
async fn last_result(&self) -> ClientResult<ExecResult> {
Ok(self.kernel.last_result().await)
}
async fn reset(&self) -> ClientResult<()> {
self.kernel
.reset()
.await
.map_err(ClientError::Other)
}
async fn ping(&self) -> ClientResult<String> {
Ok("pong".to_string())
}
async fn shutdown(&self) -> ClientResult<()> {
Ok(())
}
async fn read_blob(&self, id: &str) -> ClientResult<Vec<u8>> {
let vfs = self.kernel.vfs();
let path = PathBuf::from(format!("/v/blobs/{}", id));
vfs.read(&path)
.await
.map_err(ClientError::Io)
}
async fn write_blob(&self, content_type: &str, data: &[u8]) -> ClientResult<String> {
let vfs = self.kernel.vfs();
let id = generate_blob_id();
let path = PathBuf::from(format!("/v/blobs/{}", id));
let parent = Path::new("/v/blobs");
if let Err(e) = vfs.mkdir(parent).await {
if e.kind() != std::io::ErrorKind::AlreadyExists {
tracing::warn!("Failed to create blob directory: {}", e);
}
}
tracing::debug!("Creating blob {} with content type {}", id, content_type);
vfs.write(&path, data)
.await
.map_err(ClientError::Io)?;
Ok(id)
}
async fn delete_blob(&self, id: &str) -> ClientResult<bool> {
let vfs = self.kernel.vfs();
let path = PathBuf::from(format!("/v/blobs/{}", id));
match vfs.remove(&path).await {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(ClientError::Io(e)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_embedded_transient() {
let client = EmbeddedClient::transient().expect("failed to create client");
let result = client.ping().await.expect("ping failed");
assert_eq!(result, "pong");
}
#[tokio::test]
async fn test_embedded_execute() {
let client = EmbeddedClient::transient().expect("failed to create client");
let result = client.execute("echo hello").await.expect("execute failed");
assert!(result.ok());
assert_eq!(result.out.trim(), "hello");
}
#[tokio::test]
async fn test_embedded_variables() {
let client = EmbeddedClient::transient().expect("failed to create client");
client.execute("X=42").await.expect("set failed");
let value = client.get_var("X").await.expect("get failed");
assert_eq!(value, Some(Value::Int(42)));
client.set_var("Y", Value::String("hello".into())).await.expect("set_var failed");
let value = client.get_var("Y").await.expect("get failed");
assert_eq!(value, Some(Value::String("hello".into())));
let vars = client.list_vars().await.expect("list failed");
assert!(vars.iter().any(|(n, _)| n == "X"));
assert!(vars.iter().any(|(n, _)| n == "Y"));
}
#[tokio::test]
async fn test_embedded_cwd() {
let client = EmbeddedClient::transient().expect("failed to create client");
let cwd = client.cwd().await.expect("cwd failed");
let home = std::env::var("HOME").unwrap_or_else(|_| "/".to_string());
assert_eq!(cwd, home);
client.set_cwd("/tmp").await.expect("set_cwd failed");
let cwd = client.cwd().await.expect("cwd failed");
assert_eq!(cwd, "/tmp");
}
#[tokio::test]
async fn test_embedded_reset() {
let client = EmbeddedClient::transient().expect("failed to create client");
client.execute("X=1").await.expect("set failed");
assert!(client.get_var("X").await.expect("get failed").is_some());
client.reset().await.expect("reset failed");
assert!(client.get_var("X").await.expect("get failed").is_none());
}
#[tokio::test]
async fn test_embedded_last_result() {
let client = EmbeddedClient::transient().expect("failed to create client");
client.execute("echo test").await.expect("execute failed");
let last = client.last_result().await.expect("last_result failed");
assert!(last.ok());
assert_eq!(last.out.trim(), "test");
}
#[tokio::test]
async fn test_embedded_blob_write_read() {
let client = EmbeddedClient::transient().expect("failed to create client");
let data = b"hello blob world!";
let id = client.write_blob("text/plain", data).await.expect("write_blob failed");
assert!(!id.is_empty(), "blob id should not be empty");
let read_data = client.read_blob(&id).await.expect("read_blob failed");
assert_eq!(read_data, data);
}
#[tokio::test]
async fn test_embedded_blob_delete() {
let client = EmbeddedClient::transient().expect("failed to create client");
let data = b"blob to delete";
let id = client.write_blob("application/octet-stream", data).await.expect("write_blob failed");
let read_data = client.read_blob(&id).await.expect("read_blob failed");
assert_eq!(read_data, data);
let deleted = client.delete_blob(&id).await.expect("delete_blob failed");
assert!(deleted, "blob should have been deleted");
let result = client.read_blob(&id).await;
assert!(result.is_err(), "blob should not exist after deletion");
}
#[tokio::test]
async fn test_embedded_blob_delete_nonexistent() {
let client = EmbeddedClient::transient().expect("failed to create client");
let deleted = client.delete_blob("nonexistent-blob-id").await.expect("delete_blob failed");
assert!(!deleted, "deleting nonexistent blob should return false");
}
#[tokio::test]
async fn test_embedded_blob_large_data() {
let client = EmbeddedClient::transient().expect("failed to create client");
let data: Vec<u8> = (0..1024 * 1024).map(|i| (i % 256) as u8).collect();
let id = client.write_blob("application/octet-stream", &data).await.expect("write_blob failed");
let read_data = client.read_blob(&id).await.expect("read_blob failed");
assert_eq!(read_data.len(), data.len());
assert_eq!(read_data, data);
}
}