kftray-helper 0.23.5

Privileged helper binary for KFTray
Documentation
use std::{
    fs,
    io::Write,
    path::PathBuf,
    process::Command,
};

use kftray_commons::utils::config_dir;

use crate::{
    address_pool::AddressPoolManager,
    communication::{
        get_default_socket_path,
        start_communication_server,
    },
    error::HelperError,
    network::NetworkConfigManager,
};

const SYSTEMD_SERVICE_TEMPLATE: &str = r#"[Unit]
Description=KFTray privileged helper service
After=network.target

[Service]
Type=simple
ExecStart={{HELPER_PATH}} service
Restart=on-failure
RestartSec=5
StandardOutput=file:{{LOG_DIR}}/kftray-helper.log
StandardError=file:{{LOG_DIR}}/kftray-helper.err
# Environment variables for consistent socket path location
Environment="CONFIG_DIR={{LOG_DIR}}"
Environment="KFTRAY_CONFIG={{LOG_DIR}}"
Environment="SOCKET_PATH={{LOG_DIR}}/{{SOCKET_FILENAME}}"
Environment="USER={{CURRENT_USER}}"
Environment="HOME={{HOME_DIR}}"

[Install]
WantedBy=default.target
"#;

const POLKIT_POLICY_TEMPLATE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
 "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
  <action id="com.kftray.helper.network">
    <description>KFTray network configuration</description>
    <message>Authentication is required to configure network interfaces</message>
    <defaults>
      <allow_any>auth_admin</allow_any>
      <allow_inactive>auth_admin</allow_inactive>
      <allow_active>auth_admin_keep</allow_active>
    </defaults>
    <annotate key="org.freedesktop.policykit.exec.path">{{HELPER_PATH}}</annotate>
  </action>
</policyconfig>
"#;

pub fn install_service(service_name: &str) -> Result<(), HelperError> {
    let helper_path = std::env::current_exe().map_err(|e| {
        HelperError::PlatformService(format!("Failed to get current executable path: {}", e))
    })?;
    let helper_path_str = helper_path.to_string_lossy();

    let config_dir_path = match config_dir::get_config_dir() {
        Ok(path) => path,
        Err(_) => PathBuf::from("/tmp"),
    };

    if !config_dir_path.exists() {
        println!("Creating config directory: {}", config_dir_path.display());
        fs::create_dir_all(&config_dir_path).map_err(|e| {
            HelperError::PlatformService(format!("Failed to create config directory: {}", e))
        })?;
    }

    let current_user = std::env::var("USER").unwrap_or_else(|_| "nobody".to_string());
    let home_dir = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());

    let socket_filename = crate::communication::SOCKET_FILENAME;

    let service_content = SYSTEMD_SERVICE_TEMPLATE
        .replace("{{HELPER_PATH}}", &helper_path_str)
        .replace("{{LOG_DIR}}", &config_dir_path.to_string_lossy())
        .replace("{{SOCKET_FILENAME}}", socket_filename)
        .replace("{{CURRENT_USER}}", &current_user)
        .replace("{{HOME_DIR}}", &home_dir);

    let policy_content = POLKIT_POLICY_TEMPLATE.replace("{{HELPER_PATH}}", &helper_path_str);

    let service_path = get_systemd_service_path(service_name)?;
    if let Some(parent) = service_path.parent() {
        fs::create_dir_all(parent).map_err(|e| {
            HelperError::PlatformService(format!("Failed to create systemd directory: {}", e))
        })?;
    }

    let mut file = fs::File::create(&service_path).map_err(|e| {
        HelperError::PlatformService(format!("Failed to create systemd service file: {}", e))
    })?;
    file.write_all(service_content.as_bytes()).map_err(|e| {
        HelperError::PlatformService(format!("Failed to write systemd service file: {}", e))
    })?;

    let policy_path = get_polkit_policy_path(service_name)?;
    if let Some(parent) = policy_path.parent() {
        fs::create_dir_all(parent).map_err(|e| {
            HelperError::PlatformService(format!("Failed to create polkit directory: {}", e))
        })?;
    }

    let mut file = fs::File::create(&policy_path).map_err(|e| {
        HelperError::PlatformService(format!("Failed to create polkit policy file: {}", e))
    })?;
    file.write_all(policy_content.as_bytes()).map_err(|e| {
        HelperError::PlatformService(format!("Failed to write polkit policy file: {}", e))
    })?;

    let output = Command::new("systemctl")
        .args(["--user", "daemon-reload"])
        .output()
        .map_err(|e| {
            HelperError::PlatformService(format!("Failed to reload systemd daemon: {}", e))
        })?;

    if !output.status.success() {
        let error = String::from_utf8_lossy(&output.stderr);
        return Err(HelperError::PlatformService(format!(
            "Failed to reload systemd daemon: {}",
            error
        )));
    }

    let output = Command::new("systemctl")
        .args([
            "--user",
            "enable",
            "--now",
            &format!("{}.service", service_name),
        ])
        .output()
        .map_err(|e| {
            HelperError::PlatformService(format!("Failed to enable systemd service: {}", e))
        })?;

    if !output.status.success() {
        let error = String::from_utf8_lossy(&output.stderr);
        return Err(HelperError::PlatformService(format!(
            "Failed to enable systemd service: {}",
            error
        )));
    }

    Ok(())
}

pub fn uninstall_service(service_name: &str) -> Result<(), HelperError> {
    let output = Command::new("systemctl")
        .args([
            "--user",
            "disable",
            "--now",
            &format!("{}.service", service_name),
        ])
        .output();

    if let Err(e) = output {
        eprintln!("Warning: Failed to disable systemd service: {}", e);
    } else if let Ok(output) = output {
        if !output.status.success() {
            let error = String::from_utf8_lossy(&output.stderr);
            eprintln!("Warning: Failed to disable systemd service: {}", error);
        }
    }

    let service_path = get_systemd_service_path(service_name)?;
    if service_path.exists() {
        fs::remove_file(&service_path).map_err(|e| {
            HelperError::PlatformService(format!("Failed to remove systemd service file: {}", e))
        })?;
    }

    let policy_path = get_polkit_policy_path(service_name)?;
    if policy_path.exists() {
        fs::remove_file(&policy_path).map_err(|e| {
            HelperError::PlatformService(format!("Failed to remove polkit policy file: {}", e))
        })?;
    }

    let _ = Command::new("systemctl")
        .args(["--user", "daemon-reload"])
        .output();

    Ok(())
}

pub fn run_service() -> Result<(), HelperError> {
    println!("Starting helper service on Linux...");

    if tokio::runtime::Handle::try_current().is_ok() {
        println!("Using existing tokio runtime");
        tokio::task::block_in_place(|| {
            tokio::runtime::Handle::current().block_on(async {
                let (pool_manager, network_manager, socket_path) =
                    super::common::initialize_components().await?;

                super::common::run_communication_server(pool_manager, network_manager, socket_path)
                    .await
            })
        })
    } else {
        match tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
        {
            Ok(runtime) => {
                println!("Successfully created tokio runtime");
                runtime.block_on(async {
                    let (pool_manager, network_manager, socket_path) =
                        super::common::initialize_components().await?;

                    super::common::run_communication_server(
                        pool_manager,
                        network_manager,
                        socket_path,
                    )
                    .await
                })
            }
            Err(e) => {
                eprintln!("Failed to build tokio runtime: {}", e);
                Err(HelperError::PlatformService(format!(
                    "Failed to build tokio runtime: {}",
                    e
                )))
            }
        }
    }
}

fn get_systemd_service_path(service_name: &str) -> Result<PathBuf, HelperError> {
    let home_dir = dirs::home_dir()
        .ok_or_else(|| HelperError::PlatformService("Could not determine home directory".into()))?;

    let service_path = home_dir
        .join(".config/systemd/user")
        .join(format!("{}.service", service_name));

    Ok(service_path)
}

fn get_polkit_policy_path(service_name: &str) -> Result<PathBuf, HelperError> {
    let home_dir = dirs::home_dir()
        .ok_or_else(|| HelperError::PlatformService("Could not determine home directory".into()))?;

    let policy_path = home_dir
        .join(".local/share/polkit-1/actions")
        .join(format!("com.kftray.{}.policy", service_name));

    Ok(policy_path)
}