ipc-broker 1.1.5

A lightweight IPC broker for Rust that supports multiple transports (TCP, Unix sockets, Named Pipes) and provides an actor model with message passing and RPC capabilities.
Documentation
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::task::spawn_blocking;

use serde::Deserialize;
use tokio::sync::Mutex;

use crate::rpc::{ClientId, RpcRequest};

#[derive(Debug, Deserialize)]
pub struct ServiceActivationFile {
    #[serde(rename = "type")]
    kind: String,

    object_name: String,
    service_name: String,
}

#[derive(Clone, Debug, PartialEq)]
pub enum ServiceState {
    Stopped,
    Starting,
    Running(ClientId),
}

#[derive(Debug)]
pub struct ServiceEntry {
    pub state: ServiceState,
    pub service_name: String,
    pub pending_calls: Vec<RpcRequest>,
    pub owner: Option<ClientId>,
}

pub type SharedServices = Arc<Mutex<HashMap<String, ServiceEntry>>>;
pub const BASE_DIR: &str = "service-activation";

fn home_dir() -> Option<PathBuf> {
    dirs::home_dir()
}

pub async fn load_service_activations(services: &SharedServices) {
    let base_dir = match home_dir() {
        Some(h) => h.join(BASE_DIR),
        None => {
            log::warn!("Could not determine home directory for service activations");
            return;
        }
    };

    let entries = match fs::read_dir(&base_dir) {
        Ok(e) => e,
        Err(e) => {
            log::warn!(
                "Service activation directory {:?} not accessible: {e}",
                base_dir
            );
            return;
        }
    };

    for entry in entries.flatten() {
        let path = entry.path();

        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }

        let content = match fs::read_to_string(&path) {
            Ok(c) => c,
            Err(e) => {
                log::error!("Failed to read {:?}: {e}", path);
                continue;
            }
        };

        let parsed: Vec<ServiceActivationFile> = match serde_json::from_str(&content) {
            Ok(v) => v,
            Err(e) => {
                log::error!("Invalid JSON in {:?}: {e}", path);
                continue;
            }
        };

        for item in parsed {
            if item.kind != "RegisterService" {
                log::warn!("Ignoring {:?}: unsupported type {}", path, item.kind);
                continue;
            }

            log::info!(
                "Loaded service activation: {} -> {}",
                item.object_name,
                item.service_name
            );

            services.lock().await.insert(
                item.object_name,
                ServiceEntry {
                    state: ServiceState::Stopped,
                    service_name: item.service_name,
                    pending_calls: Vec::new(),
                    owner: None,
                },
            );
        }
    }
}

pub async fn spawn_service(unit: String) {
    // Run systemctl restart via sudo in a blocking thread to avoid blocking
    // the Tokio runtime.
    let join_res = spawn_blocking(move || {
        std::process::Command::new("/usr/bin/sudo")
            .arg("/usr/bin/systemctl")
            .arg("restart")
            .arg(unit)
            .status()
    })
    .await;

    match join_res {
        Ok(Ok(s)) if s.success() => {
            log::info!("Restarted systemd service via sudo");
        }
        Ok(Ok(s)) => {
            log::error!("sudo systemctl restart failed: {}", s);
        }
        Ok(Err(e)) => {
            log::error!("Failed to execute sudo systemctl: {}", e);
        }
        Err(e) => {
            log::error!("spawn_blocking task failed: {}", e);
        }
    }
}