feat: add local AIO agent over Unix socket
This commit is contained in:
Generated
+9
@@ -128,6 +128,15 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
|
||||
|
||||
[[package]]
|
||||
name = "enuxia-aio-agent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"enuxia-aio-protocol",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enuxia-aio-controller"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/agent",
|
||||
"crates/controller",
|
||||
"crates/protocol",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "enuxia-aio-agent"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
enuxia-aio-protocol = {
|
||||
path = "../protocol"
|
||||
}
|
||||
|
||||
serde_json = "1"
|
||||
|
||||
tokio = {
|
||||
version = "1",
|
||||
features = [
|
||||
"io-util",
|
||||
"macros",
|
||||
"net",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
use std::{
|
||||
env, fs, io,
|
||||
os::unix::fs::{FileTypeExt, PermissionsExt},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use enuxia_aio_protocol::{
|
||||
AgentHealthResponse, AgentRequest, AgentResponse, AgentStatus, ApiError, PROTOCOL_VERSION,
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
net::{UnixListener, UnixStream},
|
||||
};
|
||||
|
||||
const MAX_REQUEST_SIZE: usize = 64 * 1024;
|
||||
|
||||
struct SocketCleanup {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Drop for SocketCleanup {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
fn default_socket_path() -> PathBuf {
|
||||
let runtime_directory = env::var_os("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("/run/user/1000"));
|
||||
|
||||
runtime_directory
|
||||
.join("enuxia-frappe-aio")
|
||||
.join("agent.sock")
|
||||
}
|
||||
|
||||
fn prepare_socket_path(path: &Path) -> io::Result<()> {
|
||||
let parent = path.parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"le socket doit avoir un répertoire parent",
|
||||
)
|
||||
})?;
|
||||
|
||||
fs::create_dir_all(parent)?;
|
||||
|
||||
fs::set_permissions(parent, fs::Permissions::from_mode(0o700))?;
|
||||
|
||||
match fs::symlink_metadata(path) {
|
||||
Ok(metadata) => {
|
||||
if !metadata.file_type().is_socket() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
format!("{} existe mais n’est pas un socket Unix", path.display(),),
|
||||
));
|
||||
}
|
||||
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
||||
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn error_response(code: impl Into<String>, message: impl Into<String>) -> AgentResponse {
|
||||
AgentResponse::Error(ApiError {
|
||||
code: code.into(),
|
||||
message: message.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn process_request(request: AgentRequest) -> AgentResponse {
|
||||
match request {
|
||||
AgentRequest::Health { protocol_version } => {
|
||||
if protocol_version != PROTOCOL_VERSION {
|
||||
return error_response(
|
||||
"unsupported_protocol_version",
|
||||
format!(
|
||||
"Version reçue : {protocol_version}, \
|
||||
version attendue : {PROTOCOL_VERSION}.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AgentResponse::Health(AgentHealthResponse {
|
||||
protocol_version: PROTOCOL_VERSION,
|
||||
|
||||
agent_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
|
||||
status: AgentStatus::Ready,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(stream: UnixStream) -> io::Result<()> {
|
||||
let (reader, mut writer) = stream.into_split();
|
||||
|
||||
let mut reader = BufReader::new(reader);
|
||||
let mut request_line = String::new();
|
||||
|
||||
let bytes_read = reader.read_line(&mut request_line).await?;
|
||||
|
||||
let response = if bytes_read == 0 {
|
||||
error_response("empty_request", "La requête est vide.")
|
||||
} else if request_line.len() > MAX_REQUEST_SIZE {
|
||||
error_response(
|
||||
"request_too_large",
|
||||
"La requête dépasse la taille autorisée.",
|
||||
)
|
||||
} else {
|
||||
match serde_json::from_str::<AgentRequest>(request_line.trim_end()) {
|
||||
Ok(request) => process_request(request),
|
||||
|
||||
Err(error) => error_response(
|
||||
"invalid_request",
|
||||
format!("Requête JSON invalide : {error}"),
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
let mut payload = serde_json::to_vec(&response).map_err(io::Error::other)?;
|
||||
|
||||
payload.push(b'\n');
|
||||
|
||||
writer.write_all(&payload).await?;
|
||||
writer.shutdown().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
let socket_path = env::var_os("ENUXIA_AGENT_SOCKET")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(default_socket_path);
|
||||
|
||||
prepare_socket_path(&socket_path)?;
|
||||
|
||||
let listener = UnixListener::bind(&socket_path)?;
|
||||
|
||||
fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o600))?;
|
||||
|
||||
let _cleanup = SocketCleanup {
|
||||
path: socket_path.clone(),
|
||||
};
|
||||
|
||||
println!("Enuxia Frappe AIO Agent");
|
||||
println!("Socket : {}", socket_path.display(),);
|
||||
println!("Protocole : v{}", PROTOCOL_VERSION,);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = listener.accept() => {
|
||||
let (stream, _) = result?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) =
|
||||
handle_connection(stream).await
|
||||
{
|
||||
eprintln!(
|
||||
"Erreur de connexion : {error}"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
result = tokio::signal::ctrl_c() => {
|
||||
result?;
|
||||
|
||||
println!(
|
||||
"Arrêt demandé, fermeture de l’agent."
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,6 +2,19 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const PROTOCOL_VERSION: u16 = 1;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "action", rename_all = "snake_case")]
|
||||
pub enum AgentRequest {
|
||||
Health { protocol_version: u16 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
||||
pub enum AgentResponse {
|
||||
Health(AgentHealthResponse),
|
||||
Error(ApiError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentStatus {
|
||||
|
||||
Reference in New Issue
Block a user