joularcore 0.1.0

Joular Core is a platform to measure power and energy across all systems, OSes and devices
Documentation
/*
 * Copyright (c) 2025-2026, Adel Noureddine.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the
 * GNU Lesser General Public License v3.0 only (LGPL-3.0-only)
 * which accompanies this distribution, and is available at
 * https://www.gnu.org/licenses/lgpl-3.0.en.html
 *
 * Author : Adel Noureddine
 */

#[cfg(feature = "api")]
use crate::api;
use crate::energy::{CPUEnergy, GPUEnergy, PlatformEnergy};
#[cfg(feature = "vm")]
use crate::vm;
use crate::{Component, args::Args, platform, ringbuffer::RingBufferWriter};

#[cfg(feature = "api")]
use std::{sync::mpsc, thread};

#[cfg(feature = "api")]
use tokio::sync::broadcast::Sender;

#[cfg(feature = "api")]
pub type ApiSender = Option<Sender<api::ApiData>>;
#[cfg(not(feature = "api"))]
pub type ApiSender = Option<()>;

#[cfg(feature = "api")]
pub type ApiShutdownTx = Option<tokio::sync::oneshot::Sender<()>>;
#[cfg(not(feature = "api"))]
pub type ApiShutdownTx = Option<()>;

pub struct JoularContext {
    pub cpu_energy: Box<dyn CPUEnergy>,
    pub gpu_energy: Box<dyn GPUEnergy>,
    pub platform: Box<dyn PlatformEnergy>,
    pub ringbuffer: Option<RingBufferWriter>,
    pub api_sender: ApiSender,
    /// Kept alive so the API server runs until the process exits.
    pub api_shutdown_tx: ApiShutdownTx,
}

struct DisabledCpuEnergy;

impl CPUEnergy for DisabledCpuEnergy {
    fn get_power(&self) -> f64 {
        0.0
    }
}

struct DisabledGpuEnergy;

impl GPUEnergy for DisabledGpuEnergy {
    fn get_power(&self) -> f64 {
        0.0
    }
}

pub fn setup_joularcore(args: &Args) -> JoularContext {
    let platform = platform::current(args.gui);

    // Check for ringbuffer and enable it
    let rb = if args.ringbuffer {
        match RingBufferWriter::new(true) {
            Ok(writer) => Some(writer),
            Err(e) => {
                crate::logging::print_warning(&format!(
                    "Ring buffer unavailable: {}. Continuing without ring buffer output",
                    e
                ));
                None
            }
        }
    } else {
        None
    };

    // Initialize API channel if enabled
    #[cfg(feature = "api")]
    let (api_tx, _api_rx) = if args.api_port.is_some() {
        let (tx, rx) = tokio::sync::broadcast::channel(16);
        (Some(tx), Some(rx))
    } else {
        (None, None)
    };

    // If VM feature is enabled, check if VM monitoring is configured
    #[cfg(feature = "vm")]
    let (use_vm_cpu, use_vm_gpu) = {
        let vm_cpu_enabled = std::env::var("VM_CPU_POWER_FILE").is_ok();
        let vm_gpu_enabled = std::env::var("VM_GPU_POWER_FILE").is_ok();

        if vm_cpu_enabled && !args.numeric_only && !args.gui {
            println!("\x1b[1;32m✓ VM CPU monitoring\x1b[0m");
        }
        if vm_gpu_enabled && !args.numeric_only && !args.gui {
            println!("\x1b[1;32m✓ VM GPU monitoring\x1b[0m");
        }

        (vm_cpu_enabled, vm_gpu_enabled)
    };

    // Get CPU energy source (VM or platform)
    #[cfg(feature = "vm")]
    let cpu_energy: Box<dyn CPUEnergy> = if matches!(args.component, Some(Component::Gpu)) {
        Box::new(DisabledCpuEnergy)
    } else if use_vm_cpu {
        match vm::VmCpu::from_env() {
            Ok(vm_cpu) => Box::new(vm_cpu),
            Err(e) => {
                crate::logging::print_warning(&format!(
                    "VM CPU monitoring failed ({}); falling back to platform CPU monitoring",
                    e
                ));
                platform.cpu()
            }
        }
    } else {
        platform.cpu()
    };

    #[cfg(not(feature = "vm"))]
    let cpu_energy: Box<dyn CPUEnergy> = if matches!(args.component, Some(Component::Gpu)) {
        Box::new(DisabledCpuEnergy)
    } else {
        platform.cpu()
    };

    // Get GPU energy source (VM or platform)
    #[cfg(feature = "vm")]
    let gpu_energy: Box<dyn GPUEnergy> = if matches!(args.component, Some(Component::Cpu)) {
        Box::new(DisabledGpuEnergy)
    } else if use_vm_gpu {
        match vm::VmGpu::from_env() {
            Ok(vm_gpu) => Box::new(vm_gpu),
            Err(e) => {
                crate::logging::print_warning(&format!(
                    "VM GPU monitoring failed ({}); falling back to platform GPU monitoring",
                    e
                ));
                platform.gpu()
            }
        }
    } else {
        platform.gpu()
    };

    #[cfg(not(feature = "vm"))]
    let gpu_energy: Box<dyn GPUEnergy> = if matches!(args.component, Some(Component::Cpu)) {
        Box::new(DisabledGpuEnergy)
    } else {
        platform.gpu()
    };

    #[cfg(feature = "api")]
    let (api_sender, api_shutdown_tx) =
        if let (Some(port), Some(tx)) = (args.api_port, api_tx.clone()) {
            match spawn_api_server(port, args.api_allowed_origins.clone(), tx) {
                Ok(server) => server,
                Err(e) => {
                    crate::logging::print_error(&format!(
                        "Failed to start API server on port {}: {}. Continuing without API output",
                        port, e
                    ));
                    (None, None)
                }
            }
        } else {
            (None, None)
        };

    #[cfg(not(feature = "api"))]
    let api_sender: Option<()> = None;
    #[cfg(not(feature = "api"))]
    let api_shutdown_tx: ApiShutdownTx = None;

    JoularContext {
        cpu_energy,
        gpu_energy,
        platform,
        ringbuffer: rb,
        api_sender,
        api_shutdown_tx,
    }
}

/// Spawn the HTTP / WebSocket API server on a background thread with its own
/// tokio runtime, and return a clone of the broadcast sender that pushes new
/// samples to it. Returns only after the server has successfully bound its
/// listener, so callers do not mark the API active on bind/runtime failure.
///
/// Used by both `setup_joularcore` (CLI / GUI startup with --api-port) and the
/// GUI options screen (lazy enablement at first "Start Monitoring" click).
#[cfg(feature = "api")]
pub fn spawn_api_server(
    port: u16,
    allowed_origins: Vec<String>,
    tx: tokio::sync::broadcast::Sender<api::ApiData>,
) -> Result<(ApiSender, ApiShutdownTx), String> {
    let tx_clone = tx.clone();
    let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
    let (startup_tx, startup_rx) = mpsc::channel();

    thread::spawn(move || {
        let rt = match tokio::runtime::Runtime::new() {
            Ok(rt) => rt,
            Err(e) => {
                let _ = startup_tx.send(Err(format!("failed to start Tokio runtime: {e}")));
                return;
            }
        };
        rt.block_on(async {
            let listener = match tokio::net::TcpListener::bind(format!("127.0.0.1:{port}")).await {
                Ok(listener) => listener,
                Err(e) => {
                    let _ = startup_tx.send(Err(format!("failed to bind 127.0.0.1:{port}: {e}")));
                    return;
                }
            };

            if startup_tx.send(Ok(())).is_err() {
                return;
            }

            if let Err(e) =
                api::start_api_server(port, allowed_origins, tx, shutdown_rx, listener).await
            {
                crate::logging::print_error(&format!("API server error on port {}: {}", port, e));
            }
        });
    });

    match startup_rx.recv() {
        Ok(Ok(())) => Ok((Some(tx_clone), Some(shutdown_tx))),
        Ok(Err(e)) => Err(e),
        Err(e) => Err(format!(
            "API server startup thread ended before reporting status: {e}"
        )),
    }
}