Commit 370a28fd authored by Roman Alifanov's avatar Roman Alifanov

Initial commit

parents
This diff is collapsed. Click to expand it.
[package]
name = "eepm-dbus"
version = "0.1.0"
edition = "2021"
authors = ["Etersoft <info@etersoft.ru>"]
description = "D-Bus service for EEPM"
license = "GPL-3.0"
repository = "https://github.com/Etersoft/eepm-dbus"
[dependencies]
# D-Bus
zbus = { version = "4", default-features = false, features = ["tokio"] }
zvariant = "4"
# Async runtime
tokio = { version = "1", features = ["full", "signal", "process"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# CLI
clap = { version = "4", features = ["derive"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Error handling
thiserror = "1"
anyhow = "1"
# Utils
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
async-trait = "0.1"
futures = "0.3"
nix = { version = "0.29", features = ["user"] }
regex = "1"
[profile.release]
opt-level = 3
lto = true
strip = true
This diff is collapsed. Click to expand it.
[Unit]
Description=EEPM D-Bus Service
Documentation=man:epm(1)
After=network.target
[Service]
Type=dbus
BusName=ru.etersoft.EPM
ExecStart=@BINDIR@/eepm-dbus dbus-system
User=root
Restart=on-failure
RestartSec=5
# Package manager needs full system access
NoNewPrivileges=false
PrivateTmp=true
[Install]
WantedBy=multi-user.target
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- Only root can own the service -->
<policy user="root">
<allow own="ru.etersoft.EPM"/>
</policy>
<!-- Allow anyone to call methods -->
<policy context="default">
<!-- New interfaces -->
<allow send_destination="ru.etersoft.EPM"
send_interface="ru.etersoft.EPM.Query"/>
<allow send_destination="ru.etersoft.EPM"
send_interface="ru.etersoft.EPM.Manage"/>
<!-- Receive signals -->
<allow receive_sender="ru.etersoft.EPM"/>
<!-- Standard interfaces -->
<allow send_destination="ru.etersoft.EPM"
send_interface="org.freedesktop.DBus.Introspectable"/>
<allow send_destination="ru.etersoft.EPM"
send_interface="org.freedesktop.DBus.Properties"/>
<allow send_destination="ru.etersoft.EPM"
send_interface="org.freedesktop.DBus.Peer"/>
</policy>
</busconfig>
<?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.0/policyconfig.dtd">
<policyconfig>
<vendor>Etersoft</vendor>
<vendor_url>https://etersoft.ru</vendor_url>
<action id="ru.etersoft.EPM.manage">
<description>Manage system packages with EPM</description>
<description xml:lang="ru">Управление системными пакетами через EPM</description>
<message>Authentication is required to manage packages</message>
<message xml:lang="ru">Для управления пакетами требуется аутентификация</message>
<icon_name>package-x-generic</icon_name>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>
[D-BUS Service]
Name=ru.etersoft.EPM
Exec=@BINDIR@/eepm-dbus dbus-system
User=root
SystemdService=eepm-dbus.service
project('eepm-dbus', 'rust',
version: '0.1.0',
license: 'GPL-3.0',
meson_version: '>= 0.59.0',
)
cargo = find_program('cargo')
prefix = get_option('prefix')
bindir = prefix / get_option('bindir')
datadir = prefix / get_option('datadir')
sysconfdir = get_option('sysconfdir')
conf_data = configuration_data()
conf_data.set('BINDIR', bindir)
conf_data.set('VERSION', meson.project_version())
rust_sources_result = run_command(
'find', meson.project_source_root() / 'src', '-name', '*.rs', '-type', 'f',
check: true,
)
rust_sources_list = rust_sources_result.stdout().strip().split('\n')
rust_sources = files('Cargo.toml', 'Cargo.lock')
foreach f : rust_sources_list
rust_sources += files(f.replace(meson.project_source_root() + '/', ''))
endforeach
cargo_build = custom_target('eepm-dbus',
output: 'eepm-dbus',
depend_files: rust_sources,
command: [
'sh', '-c',
cargo.full_path() + ' build --release --manifest-path ' + meson.project_source_root() / 'Cargo.toml' +
' --target-dir ' + meson.project_source_root() / 'target' +
' && cp ' + meson.project_source_root() / 'target' / 'release' / 'eepm-dbus' + ' @OUTPUT@'
],
console: true,
build_by_default: true,
install: true,
install_dir: bindir,
)
configure_file(
input: 'data/ru.etersoft.EPM.service.in',
output: 'ru.etersoft.EPM.service',
configuration: conf_data,
install_dir: datadir / 'dbus-1' / 'system-services',
)
configure_file(
input: 'data/ru.etersoft.EPM.conf.in',
output: 'ru.etersoft.EPM.conf',
configuration: conf_data,
install_dir: sysconfdir / 'dbus-1' / 'system.d',
)
configure_file(
input: 'data/ru.etersoft.EPM.policy.in',
output: 'ru.etersoft.EPM.policy',
configuration: conf_data,
install_dir: datadir / 'polkit-1' / 'actions',
)
configure_file(
input: 'data/eepm-dbus.service.in',
output: 'eepm-dbus.service',
configuration: conf_data,
install_dir: prefix / 'lib' / 'systemd' / 'system',
)
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "eepm-dbus")]
#[command(author = "Etersoft")]
#[command(version)]
#[command(about = "D-Bus service and client for EEPM")]
pub struct Cli {
#[arg(long, global = true, help = "Output in JSON format")]
pub json: bool,
#[arg(long, short = 'p', global = true, help = "Show progress bar")]
pub progress: bool,
#[arg(short, long, global = true, help = "Quiet mode")]
pub quiet: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Start D-Bus system service
DbusSystem,
/// Query commands (no authorization required)
#[command(visible_alias = "q")]
Query {
#[command(subcommand)]
cmd: QueryCommands,
},
/// Manage commands (requires authorization)
#[command(visible_alias = "m")]
Manage {
#[command(subcommand)]
cmd: ManageCommands,
},
/// Play query commands (no authorization required)
#[command(visible_alias = "pq")]
PlayQuery {
#[command(subcommand)]
cmd: PlayQueryCommands,
},
/// Play manage commands (requires authorization)
#[command(visible_alias = "pm")]
PlayManage {
#[command(subcommand)]
cmd: PlayManageCommands,
},
}
#[derive(Subcommand, Debug)]
pub enum QueryCommands {
/// Search for packages
Search {
query: String,
#[arg(long, short, help = "Search only installed")]
installed: bool,
},
/// Show package information
Info { package: String },
/// List installed packages
List {
#[arg(long, default_value = "0")]
limit: i32,
#[arg(long, default_value = "0")]
offset: i32,
},
/// Show package dependencies
Requires { package: String },
/// Show reverse dependencies
WhatDepends { package: String },
/// Show package file list
FileList { package: String },
/// Check/simulate package installation
CheckInstall {
#[arg(required = true)]
packages: Vec<String>,
},
/// Check/simulate package removal
CheckRemove {
#[arg(required = true)]
packages: Vec<String>,
#[arg(long, help = "Include dependencies")]
depends: bool,
},
/// Check available upgrades
CheckUpgrade,
/// Check autoremove candidates
CheckAutoRemove,
/// Check orphan packages
CheckAutoOrphans,
/// Check kernel update
CheckUpdateKernel,
/// Check system integrity
Check,
}
#[derive(Subcommand, Debug)]
pub enum ManageCommands {
/// Install packages
Install {
#[arg(required = true)]
packages: Vec<String>,
},
/// Remove packages
Remove {
#[arg(required = true)]
packages: Vec<String>,
#[arg(long, help = "Remove config files too")]
purge: bool,
#[arg(long, help = "Remove unused dependencies")]
depends: bool,
},
/// Download packages without installing
Download {
#[arg(required = true)]
packages: Vec<String>,
},
/// Update package indexes
Update,
/// Upgrade system packages
Upgrade,
/// Clean package cache
Clean,
/// Remove unused packages
AutoRemove,
/// Remove orphan packages
AutoOrphans,
/// Update kernel
UpdateKernel,
}
#[derive(Subcommand, Debug)]
pub enum PlayQueryCommands {
/// List installed play applications
List,
/// List all available play applications
ListAll,
/// List system scripts
ListScripts,
/// Search play applications
Search { query: String },
/// Show play application info
Info { app: String },
/// Check if play app is installed
Installed { app: String },
/// Get available version
AvailableVersion { app: String },
/// Get installed version
InstalledVersion { app: String },
}
#[derive(Subcommand, Debug)]
pub enum PlayManageCommands {
/// Install play application
Install { app: String },
/// Remove play application
Remove { app: String },
/// Update play application
Update {
#[arg(help = "App name or 'all'")]
app: String,
},
}
pub mod args;
pub mod output;
pub use args::*;
pub use output::*;
use serde::Serialize;
pub enum OutputFormat {
Text,
Json,
}
#[allow(dead_code)]
pub fn print_output<T: Serialize + std::fmt::Debug>(data: &T, format: OutputFormat) {
match format {
OutputFormat::Json => {
if let Ok(json) = serde_json::to_string_pretty(data) {
println!("{}", json);
}
}
OutputFormat::Text => {
println!("{:#?}", data);
}
}
}
/// Print JSON response from D-Bus service
pub fn print_json_response(json_str: &str, format: OutputFormat) {
match format {
OutputFormat::Json => {
// Pretty print the JSON
if let Ok(value) = serde_json::from_str::<serde_json::Value>(json_str) {
if let Ok(pretty) = serde_json::to_string_pretty(&value) {
println!("{}", pretty);
return;
}
}
// Fallback: print as-is
println!("{}", json_str);
}
OutputFormat::Text => {
// Parse JSON and print in debug format
if let Ok(value) = serde_json::from_str::<serde_json::Value>(json_str) {
println!("{:#?}", value);
} else {
println!("{}", json_str);
}
}
}
}
pub fn print_error(error: &str, format: OutputFormat) {
match format {
OutputFormat::Json => {
let err = serde_json::json!({
"error": true,
"message": error
});
eprintln!("{}", err);
}
OutputFormat::Text => {
eprintln!("Error: {}", error);
}
}
}
#[allow(dead_code)]
pub fn print_success(message: &str, format: OutputFormat) {
match format {
OutputFormat::Json => {
let msg = serde_json::json!({
"error": false,
"message": message
});
println!("{}", msg);
}
OutputFormat::Text => {
println!("{}", message);
}
}
}
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Config {
pub epm_path: PathBuf,
#[allow(dead_code)]
pub version: String,
}
impl Default for Config {
fn default() -> Self {
Self {
epm_path: PathBuf::from("/usr/bin/epm"),
version: env!("CARGO_PKG_VERSION").to_string(),
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
#[allow(dead_code)]
pub fn with_epm_path(mut self, path: PathBuf) -> Self {
self.epm_path = path;
self
}
}
pub mod server;
pub mod query;
pub mod manage;
pub mod client;
pub mod signals;
pub use server::*;
pub use client::EpmClient;
pub use signals::{SignalEmitter, ProgressEvent, parse_progress_line};
use std::sync::Arc;
use tokio::sync::RwLock;
use zbus::interface;
use crate::config::Config;
use crate::eepm::commands::{info, install, play, remove, search, system};
use crate::types::ApiResponse;
fn to_json<T: serde::Serialize>(data: &T) -> zbus::fdo::Result<String> {
serde_json::to_string(data).map_err(|e| zbus::fdo::Error::Failed(e.to_string()))
}
pub struct QueryInterface {
config: Arc<RwLock<Config>>,
}
impl QueryInterface {
pub fn new(config: Arc<RwLock<Config>>) -> Self {
Self { config }
}
}
#[interface(name = "ru.etersoft.EPM.Query")]
impl QueryInterface {
async fn search(
&self,
query: String,
installed: bool,
) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = search::execute(&config, &query, installed)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn info(&self, package_name: String) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = info::execute(&config, &package_name)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn list(
&self,
limit: i32,
offset: i32,
) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = info::list(&config, limit, offset)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn requires(&self, package_name: String) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = info::requires(&config, &package_name)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn what_depends(&self, package_name: String) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = info::whatdepends(&config, &package_name)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn file_list(&self, package_name: String) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = info::filelist(&config, &package_name)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn check_install(&self, packages: Vec<String>) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = install::check(&config, &packages)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn check_remove(&self, packages: Vec<String>, depends: bool) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = remove::check(&config, &packages, depends)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn check_upgrade(&self) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = system::check_upgrade(&config)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn check_auto_remove(&self) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = system::check_autoremove(&config)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn check_auto_orphans(&self) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = system::check_autoorphans(&config)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn check_update_kernel(&self) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = system::check_update_kernel(&config)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn check(&self) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = system::check(&config)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn play_list(&self) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = play::list(&config)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn play_list_all(&self) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = play::list_all(&config)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn play_list_scripts(&self) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = play::list_scripts(&config)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn play_search(&self, query: String) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = play::search(&config, &query)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn play_info(&self, app: String) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = play::info(&config, &app)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn play_installed(&self, app: String) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = play::installed(&config, &app)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn play_available_version(&self, app: String) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = play::available_version(&config, &app)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
async fn play_installed_version(&self, app: String) -> zbus::fdo::Result<String> {
let config = self.config.read().await;
let result = play::installed_version(&config, &app)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
to_json(&ApiResponse::success(result))
}
}
use std::sync::Arc;
use tokio::sync::RwLock;
use zbus::Connection;
use crate::config::Config;
use crate::dbus::query::QueryInterface;
use crate::dbus::manage::ManageInterface;
use crate::error::Result;
pub const DBUS_NAME: &str = "ru.etersoft.EPM";
pub const DBUS_PATH: &str = "/ru/etersoft/EPM";
pub struct EpmServer {
config: Arc<RwLock<Config>>,
}
impl EpmServer {
pub fn new(config: Arc<RwLock<Config>>) -> Self {
Self { config }
}
pub async fn register(&self, connection: &Connection) -> Result<()> {
let query = QueryInterface::new(self.config.clone());
connection.object_server().at(DBUS_PATH, query).await?;
let manage = ManageInterface::new(self.config.clone(), connection.clone());
connection.object_server().at(DBUS_PATH, manage).await?;
Ok(())
}
}
//! D-Bus signal emission and progress tracking.
//!
//! EventData structure and D-Bus Notification signal concept are based on APM
//! (Atomic Package Manager) by Dmitry Udalov.
//! See: https://altlinux.space/alt-atomic/apm
//! Original: apm/internal/common/reply/event.go
use serde::{Deserialize, Serialize};
use zbus::Connection;
use tracing::debug;
#[allow(dead_code)]
pub const EVENT_TYPE_NOTIFICATION: &str = "NOTIFICATION";
pub const EVENT_TYPE_PROGRESS: &str = "PROGRESS";
#[allow(dead_code)]
pub const EVENT_TYPE_TASK_RESULT: &str = "TASK_RESULT";
pub const STATE_BEFORE: &str = "BEFORE";
pub const STATE_AFTER: &str = "AFTER";
/// Event data sent via D-Bus Notification signal.
/// Based on APM's EventData structure.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventData {
pub name: String,
#[serde(rename = "message")]
pub view: String,
pub state: String,
#[serde(rename = "type")]
pub event_type: String,
pub progress: f64,
#[serde(rename = "progressDone")]
pub progress_done: String,
pub transaction: String,
}
impl Default for EventData {
fn default() -> Self {
Self {
name: String::new(),
view: String::new(),
state: STATE_BEFORE.to_string(),
event_type: EVENT_TYPE_NOTIFICATION.to_string(),
progress: 0.0,
progress_done: String::new(),
transaction: String::new(),
}
}
}
impl EventData {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
..Default::default()
}
}
pub fn with_state(mut self, state: &str) -> Self {
self.state = state.to_string();
self
}
pub fn with_progress(mut self, percent: f64) -> Self {
self.event_type = EVENT_TYPE_PROGRESS.to_string();
self.progress = percent;
self
}
pub fn with_message(mut self, message: &str) -> Self {
self.view = message.to_string();
self
}
pub fn with_progress_done(mut self, text: &str) -> Self {
self.progress_done = text.to_string();
self
}
pub fn with_transaction(mut self, tx: &str) -> Self {
self.transaction = tx.to_string();
self
}
}
#[derive(Clone)]
pub struct SignalEmitter {
connection: Connection,
}
impl SignalEmitter {
pub fn new(connection: Connection) -> Self {
Self { connection }
}
pub async fn emit(&self, event: &EventData) -> crate::error::Result<()> {
let json = serde_json::to_string(event)?;
debug!("Emitting signal: {}", json);
self.connection
.emit_signal(
None::<&str>,
"/ru/etersoft/EPM",
"ru.etersoft.EPM",
"Notification",
&(json,),
)
.await?;
Ok(())
}
pub async fn emit_progress(
&self,
name: &str,
message: &str,
percent: f64,
transaction: &str,
) -> crate::error::Result<()> {
let event = EventData::new(name)
.with_state(STATE_BEFORE)
.with_progress(percent)
.with_message(message)
.with_transaction(transaction);
self.emit(&event).await
}
pub async fn emit_progress_done(
&self,
name: &str,
message: &str,
transaction: &str,
) -> crate::error::Result<()> {
let event = EventData::new(name)
.with_state(STATE_AFTER)
.with_progress(100.0)
.with_progress_done(message)
.with_transaction(transaction);
self.emit(&event).await
}
#[allow(dead_code)]
pub async fn emit_task_started(
&self,
name: &str,
message: &str,
transaction: &str,
) -> crate::error::Result<()> {
let event = EventData::new(name)
.with_state(STATE_BEFORE)
.with_message(message)
.with_transaction(transaction);
self.emit(&event).await
}
#[allow(dead_code)]
pub async fn emit_task_completed(
&self,
name: &str,
message: &str,
transaction: &str,
) -> crate::error::Result<()> {
let event = EventData::new(name)
.with_state(STATE_AFTER)
.with_message(message)
.with_transaction(transaction);
self.emit(&event).await
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ProgressEvent {
Download { global_percent: f64, package: String, local_percent: f64 },
Install { step: u32, package: String, percent: f64 },
Generic { message: String, percent: f64 },
}
/// Parse progress from epm/apt output line.
/// Based on APM progress parsing patterns.
pub fn parse_progress_line(line: &str) -> Option<ProgressEvent> {
use regex::Regex;
use std::sync::OnceLock;
static DOWNLOAD_RE: OnceLock<Regex> = OnceLock::new();
static INSTALL_RE: OnceLock<Regex> = OnceLock::new();
static GENERIC_RE: OnceLock<Regex> = OnceLock::new();
// "2% [10 speed-dreams-data 39368080/2037MB 1%]"
let download_re = DOWNLOAD_RE.get_or_init(|| {
Regex::new(r"(?P<global>\d+)%\s*\[(?P<order>\d+)\s+(?P<pkg>[\w\-\+\.]+)\s+(?P<data>[0-9]+/[0-9]+[KMG]?B)\s+(?P<local>\d+)%\]").unwrap()
});
// "1: erlang-otp-1:26.2.5.3-alt2 ########## [ 25%]"
let install_re = INSTALL_RE.get_or_init(|| {
Regex::new(r"^(?P<step>\d+):\s+(?P<pkg>[\w\-\:\+\.]+).*?\[\s*(?P<percent>\d+)%\]").unwrap()
});
// "Preparing... ################################# [100%]"
let generic_re = GENERIC_RE.get_or_init(|| {
Regex::new(r"^(?P<msg>[A-Za-z]+\.{3})\s+[#]+\s*\[\s*(?P<percent>\d+)%\]").unwrap()
});
let line = line.trim();
if let Some(caps) = download_re.captures(line) {
let global_percent = caps.name("global")
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0.0);
let package = caps.name("pkg")
.map(|m| m.as_str().to_string())
.unwrap_or_default();
let local_percent = caps.name("local")
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0.0);
return Some(ProgressEvent::Download {
global_percent,
package,
local_percent,
});
}
if let Some(caps) = install_re.captures(line) {
let step = caps.name("step")
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0);
let package = caps.name("pkg")
.map(|m| m.as_str().to_string())
.unwrap_or_default();
let percent = caps.name("percent")
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0.0);
return Some(ProgressEvent::Install {
step,
package,
percent,
});
}
if let Some(caps) = generic_re.captures(line) {
let message = caps.name("msg")
.map(|m| m.as_str().to_string())
.unwrap_or_default();
let percent = caps.name("percent")
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0.0);
return Some(ProgressEvent::Generic { message, percent });
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_install_progress() {
let line = "1: apa-0.1.8.alpha-alt2 ################################# [100%]";
let result = parse_progress_line(line);
assert!(result.is_some());
if let Some(ProgressEvent::Install { step, package, percent }) = result {
assert_eq!(step, 1);
assert_eq!(package, "apa-0.1.8.alpha-alt2");
assert_eq!(percent, 100.0);
} else {
panic!("Expected Install event");
}
}
#[test]
fn test_parse_preparing_progress() {
let line = "Preparing... ################################# [100%]";
let result = parse_progress_line(line);
assert!(result.is_some());
if let Some(ProgressEvent::Generic { message, percent }) = result {
assert_eq!(message, "Preparing...");
assert_eq!(percent, 100.0);
} else {
panic!("Expected Generic event");
}
}
#[test]
fn test_parse_download_progress() {
let line = "2% [10 speed-dreams-data 39368080/2037MB 1%]";
let result = parse_progress_line(line);
assert!(result.is_some());
if let Some(ProgressEvent::Download { global_percent, package, local_percent }) = result {
assert_eq!(global_percent, 2.0);
assert_eq!(package, "speed-dreams-data");
assert_eq!(local_percent, 1.0);
} else {
panic!("Expected Download event");
}
}
}
use crate::config::Config;
use crate::eepm::{EpmExecutor, parser};
use crate::error::{EpmError, Result};
use crate::types::{
DownloadResponse, FileListResponse, InfoResponse, ListResponse,
Package, RequiresResponse, WhatDependsResponse,
};
/// Get package information
pub async fn execute(config: &Config, package: &str) -> Result<InfoResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.info(package).await?;
if !output.success || output.stdout.trim().is_empty() {
return Err(EpmError::PackageNotFound(package.to_string()));
}
let pkg = parser::parse_package_info(&output.stdout)?;
Ok(InfoResponse {
message: "Package info retrieved".into(),
package: pkg,
})
}
/// List installed packages
pub async fn list(
config: &Config,
limit: i32,
offset: i32,
) -> Result<ListResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.list_installed().await?;
let all_packages = parser::parse_list_output(&output.stdout);
let total_count = all_packages.len() as u32;
// Apply pagination
let packages: Vec<Package> = all_packages
.into_iter()
.skip(offset as usize)
.take(if limit > 0 { limit as usize } else { usize::MAX })
.collect();
Ok(ListResponse {
message: format!("Listed {} packages", packages.len()),
packages,
total_count,
offset: offset as u32,
limit: limit as u32,
})
}
/// Download packages
pub async fn download(config: &Config, packages: &[String]) -> Result<DownloadResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.download(packages).await?;
if !output.success {
return Err(EpmError::Execution(format!(
"Download failed: {}",
output.stderr
)));
}
// Parse downloaded files from output
let files: Vec<String> = output
.stdout
.lines()
.filter(|l| l.ends_with(".rpm") || l.ends_with(".deb"))
.map(|l| l.to_string())
.collect();
Ok(DownloadResponse {
message: "Packages downloaded".into(),
files,
total_size: 0, // TODO: parse from output
})
}
/// Get package dependencies
pub async fn requires(config: &Config, package: &str) -> Result<RequiresResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.requires(package).await?;
let dependencies = parser::parse_requires_output(&output.stdout);
Ok(RequiresResponse {
message: format!("Found {} dependencies", dependencies.len()),
dependencies,
})
}
/// Get reverse dependencies
pub async fn whatdepends(config: &Config, package: &str) -> Result<WhatDependsResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.whatdepends(package).await?;
let dependents = parser::parse_whatdepends_output(&output.stdout);
Ok(WhatDependsResponse {
message: format!("Found {} dependent packages", dependents.len()),
dependents,
})
}
/// Get package file list
pub async fn filelist(config: &Config, package: &str) -> Result<FileListResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.filelist(package).await?;
let files = parser::parse_filelist_output(&output.stdout);
Ok(FileListResponse {
message: format!("Found {} files", files.len()),
files,
})
}
use crate::config::Config;
use crate::dbus::SignalEmitter;
use crate::eepm::{EpmExecutor, parser};
use crate::error::{EpmError, Result};
use crate::types::{CheckResponse, InstallRemoveResponse};
/// Execute package installation
#[allow(dead_code)]
pub async fn execute(
config: &Config,
packages: &[String],
) -> Result<InstallRemoveResponse> {
execute_with_signals(config, packages, None, "").await
}
/// Execute package installation with signal emission
pub async fn execute_with_signals(
config: &Config,
packages: &[String],
signal_emitter: Option<SignalEmitter>,
transaction: &str,
) -> Result<InstallRemoveResponse> {
if packages.is_empty() {
return Err(EpmError::InvalidArgument(
"At least one package must be specified".into(),
));
}
let executor = if let Some(emitter) = signal_emitter {
EpmExecutor::new(config.clone()).with_signal_emitter(emitter)
} else {
EpmExecutor::new(config.clone())
};
let output = if transaction.is_empty() {
executor.install(packages, false).await?
} else {
executor.install_with_progress(packages, transaction).await?
};
if !output.success {
return Err(EpmError::Execution(format!(
"Installation failed: {}",
output.stderr
)));
}
let changes = parser::parse_simulate_output(&output.stdout).unwrap_or_default();
Ok(InstallRemoveResponse {
message: "Packages installed successfully".into(),
info: changes,
})
}
/// Check/simulate package installation
pub async fn check(config: &Config, packages: &[String]) -> Result<CheckResponse> {
if packages.is_empty() {
return Err(EpmError::InvalidArgument(
"At least one package must be specified".into(),
));
}
let executor = EpmExecutor::new(config.clone());
let output = executor.install(packages, true).await?;
let changes = parser::parse_simulate_output(&output.stdout)?;
Ok(CheckResponse {
message: "Simulation completed".into(),
info: changes,
})
}
pub mod install;
pub mod play;
pub mod remove;
pub mod search;
pub mod info;
pub mod system;
use crate::config::Config;
use crate::dbus::SignalEmitter;
use crate::eepm::{EpmExecutor, parser};
use crate::error::{EpmError, Result};
use crate::types::{
PlayListResponse, PlaySearchResponse, PlayInfoResponse,
PlayActionResponse, PlayVersionResponse,
};
// ==================== QUERY COMMANDS (no polkit) ====================
/// List installed applications
pub async fn list(config: &Config) -> Result<PlayListResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.play_list().await?;
let mut apps = parser::parse_play_list_output(&output.stdout);
// Mark as installed since this is --list
for app in &mut apps {
app.installed = true;
}
Ok(PlayListResponse {
message: format!("Found {} installed applications", apps.len()),
total_count: apps.len() as u32,
apps,
})
}
/// List all available applications
pub async fn list_all(config: &Config) -> Result<PlayListResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.play_list_all().await?;
let apps = parser::parse_play_list_output(&output.stdout);
Ok(PlayListResponse {
message: format!("Found {} available applications", apps.len()),
total_count: apps.len() as u32,
apps,
})
}
/// List system scripts
pub async fn list_scripts(config: &Config) -> Result<PlayListResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.play_list_scripts().await?;
let apps = parser::parse_play_list_output(&output.stdout);
Ok(PlayListResponse {
message: format!("Found {} system scripts", apps.len()),
total_count: apps.len() as u32,
apps,
})
}
/// Search applications
pub async fn search(config: &Config, query: &str) -> Result<PlaySearchResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.play_search(query).await?;
let apps = parser::parse_play_list_output(&output.stdout);
Ok(PlaySearchResponse {
message: format!("Found {} applications", apps.len()),
total_count: apps.len() as u32,
apps,
})
}
/// Get application info
pub async fn info(config: &Config, app: &str) -> Result<PlayInfoResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.play_info(app).await?;
let mut app_info = parser::parse_play_info_output(&output.stdout, app);
// Check if installed
let installed_output = executor.play_installed(app).await?;
app_info.installed = installed_output.success;
// Get versions if available
if app_info.installed {
if let Ok(ver_output) = executor.play_installed_version(app).await {
app_info.installed_version = parser::parse_play_version_output(&ver_output.stdout);
}
}
if let Ok(ver_output) = executor.play_available_version(app).await {
app_info.available_version = parser::parse_play_version_output(&ver_output.stdout);
}
Ok(PlayInfoResponse {
message: "Application info retrieved".into(),
app: app_info,
})
}
/// Check if application is installed
pub async fn installed(config: &Config, app: &str) -> Result<PlayVersionResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.play_installed(app).await?;
let version = if output.success {
if let Ok(ver_output) = executor.play_installed_version(app).await {
parser::parse_play_version_output(&ver_output.stdout)
} else {
None
}
} else {
None
};
Ok(PlayVersionResponse {
version,
installed: output.success,
})
}
/// Get available version (for update checking)
pub async fn available_version(config: &Config, app: &str) -> Result<PlayVersionResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.play_available_version(app).await?;
let version = parser::parse_play_version_output(&output.stdout);
Ok(PlayVersionResponse {
version,
installed: false,
})
}
/// Get installed version
pub async fn installed_version(config: &Config, app: &str) -> Result<PlayVersionResponse> {
let executor = EpmExecutor::new(config.clone());
// First check if installed
let installed_output = executor.play_installed(app).await?;
if !installed_output.success {
return Ok(PlayVersionResponse {
version: None,
installed: false,
});
}
let output = executor.play_installed_version(app).await?;
let version = parser::parse_play_version_output(&output.stdout);
Ok(PlayVersionResponse {
version,
installed: true,
})
}
// ==================== MANAGE COMMANDS (requires polkit) ====================
/// Install application
#[allow(dead_code)]
pub async fn install(config: &Config, app: &str) -> Result<PlayActionResponse> {
install_with_signals(config, app, None, "").await
}
/// Install application with signal emission
pub async fn install_with_signals(
config: &Config,
app: &str,
signal_emitter: Option<SignalEmitter>,
transaction: &str,
) -> Result<PlayActionResponse> {
if app.is_empty() {
return Err(EpmError::InvalidArgument(
"Application name must be specified".into(),
));
}
let executor = if let Some(emitter) = signal_emitter {
EpmExecutor::new(config.clone()).with_signal_emitter(emitter)
} else {
EpmExecutor::new(config.clone())
};
let output = if transaction.is_empty() {
executor.play_install(app).await?
} else {
executor.play_install_with_progress(app, transaction).await?
};
if !output.success {
return Err(EpmError::Execution(format!(
"Installation failed: {}",
output.stderr
)));
}
Ok(PlayActionResponse {
message: format!("Application '{}' installed successfully", app),
app: app.to_string(),
transaction: transaction.to_string(),
})
}
/// Remove application
#[allow(dead_code)]
pub async fn remove(config: &Config, app: &str) -> Result<PlayActionResponse> {
remove_with_signals(config, app, None, "").await
}
/// Remove application with signal emission
pub async fn remove_with_signals(
config: &Config,
app: &str,
signal_emitter: Option<SignalEmitter>,
transaction: &str,
) -> Result<PlayActionResponse> {
if app.is_empty() {
return Err(EpmError::InvalidArgument(
"Application name must be specified".into(),
));
}
let executor = if let Some(emitter) = signal_emitter {
EpmExecutor::new(config.clone()).with_signal_emitter(emitter)
} else {
EpmExecutor::new(config.clone())
};
let output = if transaction.is_empty() {
executor.play_remove(app).await?
} else {
executor.play_remove_with_progress(app, transaction).await?
};
if !output.success {
return Err(EpmError::Execution(format!(
"Removal failed: {}",
output.stderr
)));
}
Ok(PlayActionResponse {
message: format!("Application '{}' removed successfully", app),
app: app.to_string(),
transaction: transaction.to_string(),
})
}
/// Update application (or "all" for all apps)
#[allow(dead_code)]
pub async fn update(config: &Config, app: &str) -> Result<PlayActionResponse> {
update_with_signals(config, app, None, "").await
}
/// Update application with signal emission
pub async fn update_with_signals(
config: &Config,
app: &str,
signal_emitter: Option<SignalEmitter>,
transaction: &str,
) -> Result<PlayActionResponse> {
if app.is_empty() {
return Err(EpmError::InvalidArgument(
"Application name must be specified (or 'all' for all apps)".into(),
));
}
let executor = if let Some(emitter) = signal_emitter {
EpmExecutor::new(config.clone()).with_signal_emitter(emitter)
} else {
EpmExecutor::new(config.clone())
};
let output = if transaction.is_empty() {
executor.play_update(app).await?
} else {
executor.play_update_with_progress(app, transaction).await?
};
if !output.success {
return Err(EpmError::Execution(format!(
"Update failed: {}",
output.stderr
)));
}
let message = if app == "all" {
"All applications updated successfully".to_string()
} else {
format!("Application '{}' updated successfully", app)
};
Ok(PlayActionResponse {
message,
app: app.to_string(),
transaction: transaction.to_string(),
})
}
use crate::config::Config;
use crate::dbus::SignalEmitter;
use crate::eepm::{EpmExecutor, parser};
use crate::error::{EpmError, Result};
use crate::types::{CheckResponse, InstallRemoveResponse};
/// Execute package removal
#[allow(dead_code)]
pub async fn execute(
config: &Config,
packages: &[String],
purge: bool,
depends: bool,
) -> Result<InstallRemoveResponse> {
execute_with_signals(config, packages, purge, depends, None, "").await
}
/// Execute package removal with signal emission
pub async fn execute_with_signals(
config: &Config,
packages: &[String],
purge: bool,
depends: bool,
signal_emitter: Option<SignalEmitter>,
transaction: &str,
) -> Result<InstallRemoveResponse> {
if packages.is_empty() {
return Err(EpmError::InvalidArgument(
"At least one package must be specified".into(),
));
}
let executor = if let Some(emitter) = signal_emitter {
EpmExecutor::new(config.clone()).with_signal_emitter(emitter)
} else {
EpmExecutor::new(config.clone())
};
let output = if transaction.is_empty() {
executor.remove(packages, purge, depends, false).await?
} else {
executor.remove_with_progress(packages, purge, depends, transaction).await?
};
if !output.success {
return Err(EpmError::Execution(format!(
"Removal failed: {}",
output.stderr
)));
}
let changes = parser::parse_simulate_output(&output.stdout).unwrap_or_default();
Ok(InstallRemoveResponse {
message: "Packages removed successfully".into(),
info: changes,
})
}
/// Check/simulate package removal
pub async fn check(
config: &Config,
packages: &[String],
depends: bool,
) -> Result<CheckResponse> {
if packages.is_empty() {
return Err(EpmError::InvalidArgument(
"At least one package must be specified".into(),
));
}
let executor = EpmExecutor::new(config.clone());
let output = executor.remove(packages, false, depends, true).await?;
let changes = parser::parse_simulate_output(&output.stdout)?;
Ok(CheckResponse {
message: "Simulation completed".into(),
info: changes,
})
}
use crate::config::Config;
use crate::eepm::{EpmExecutor, parser};
use crate::error::Result;
use crate::types::SearchResponse;
/// Execute package search
pub async fn execute(
config: &Config,
query: &str,
installed: bool,
) -> Result<SearchResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.search(query, installed).await?;
let packages = parser::parse_search_output(&output.stdout);
Ok(SearchResponse {
message: format!("Found {} packages", packages.len()),
total_count: packages.len() as u32,
packages,
})
}
use crate::config::Config;
use crate::dbus::SignalEmitter;
use crate::eepm::{EpmExecutor, parser};
use crate::error::{EpmError, Result};
use crate::types::{
CheckResponse, CheckSystemResponse, CleanResponse, UpdateResponse, UpgradeResponse,
KernelUpdateResponse,
};
#[allow(dead_code)]
pub async fn update(config: &Config) -> Result<UpdateResponse> {
update_with_signals(config, None, "").await
}
pub async fn update_with_signals(
config: &Config,
signal_emitter: Option<SignalEmitter>,
transaction: &str,
) -> Result<UpdateResponse> {
let executor = if let Some(emitter) = signal_emitter {
EpmExecutor::new(config.clone()).with_signal_emitter(emitter)
} else {
EpmExecutor::new(config.clone())
};
let output = if transaction.is_empty() {
executor.update().await?
} else {
executor.update_with_progress(transaction).await?
};
if !output.success {
return Err(EpmError::Execution(format!(
"Update failed: {}",
output.stderr
)));
}
Ok(UpdateResponse {
message: "Package indexes updated".into(),
count: 0, // TODO: parse actual count
})
}
#[allow(dead_code)]
pub async fn upgrade(config: &Config) -> Result<UpgradeResponse> {
upgrade_with_signals(config, None, "").await
}
pub async fn upgrade_with_signals(
config: &Config,
signal_emitter: Option<SignalEmitter>,
transaction: &str,
) -> Result<UpgradeResponse> {
let executor = if let Some(emitter) = signal_emitter {
EpmExecutor::new(config.clone()).with_signal_emitter(emitter)
} else {
EpmExecutor::new(config.clone())
};
let output = if transaction.is_empty() {
executor.upgrade(false).await?
} else {
executor.upgrade_with_progress(transaction).await?
};
if !output.success {
return Err(EpmError::Execution(format!(
"Upgrade failed: {}",
output.stderr
)));
}
let changes = parser::parse_simulate_output(&output.stdout).unwrap_or_default();
Ok(UpgradeResponse {
message: "System upgraded".into(),
info: changes,
})
}
pub async fn check_upgrade(config: &Config) -> Result<CheckResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.upgrade(true).await?;
let changes = parser::parse_simulate_output(&output.stdout)?;
Ok(CheckResponse {
message: "Upgrade simulation completed".into(),
info: changes,
})
}
pub async fn clean(config: &Config) -> Result<CleanResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.clean().await?;
if !output.success {
return Err(EpmError::Execution(format!(
"Clean failed: {}",
output.stderr
)));
}
Ok(CleanResponse {
message: "Cache cleaned".into(),
freed_space: 0, // TODO: parse from output
})
}
#[allow(dead_code)]
pub async fn autoremove(config: &Config) -> Result<UpgradeResponse> {
autoremove_with_signals(config, None, "").await
}
pub async fn autoremove_with_signals(
config: &Config,
signal_emitter: Option<SignalEmitter>,
transaction: &str,
) -> Result<UpgradeResponse> {
let executor = if let Some(emitter) = signal_emitter {
EpmExecutor::new(config.clone()).with_signal_emitter(emitter)
} else {
EpmExecutor::new(config.clone())
};
let output = if transaction.is_empty() {
executor.autoremove(false).await?
} else {
executor.autoremove_with_progress(transaction).await?
};
if !output.success {
return Err(EpmError::Execution(format!(
"Autoremove failed: {}",
output.stderr
)));
}
let changes = parser::parse_simulate_output(&output.stdout).unwrap_or_default();
Ok(UpgradeResponse {
message: "Unused packages removed".into(),
info: changes,
})
}
pub async fn check_autoremove(config: &Config) -> Result<CheckResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.autoremove(true).await?;
let changes = parser::parse_simulate_output(&output.stdout)?;
Ok(CheckResponse {
message: "Autoremove simulation completed".into(),
info: changes,
})
}
#[allow(dead_code)]
pub async fn autoorphans(config: &Config) -> Result<UpgradeResponse> {
autoorphans_with_signals(config, None, "").await
}
pub async fn autoorphans_with_signals(
config: &Config,
signal_emitter: Option<SignalEmitter>,
transaction: &str,
) -> Result<UpgradeResponse> {
let executor = if let Some(emitter) = signal_emitter {
EpmExecutor::new(config.clone()).with_signal_emitter(emitter)
} else {
EpmExecutor::new(config.clone())
};
let output = if transaction.is_empty() {
executor.autoorphans(false).await?
} else {
executor.autoorphans_with_progress(transaction).await?
};
if !output.success {
return Err(EpmError::Execution(format!(
"Autoorphans failed: {}",
output.stderr
)));
}
let changes = parser::parse_simulate_output(&output.stdout).unwrap_or_default();
Ok(UpgradeResponse {
message: "Orphan packages removed".into(),
info: changes,
})
}
pub async fn check_autoorphans(config: &Config) -> Result<CheckResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.autoorphans(true).await?;
let changes = parser::parse_simulate_output(&output.stdout)?;
Ok(CheckResponse {
message: "Autoorphans simulation completed".into(),
info: changes,
})
}
#[allow(dead_code)]
pub async fn update_kernel(config: &Config) -> Result<KernelUpdateResponse> {
update_kernel_with_signals(config, None, "").await
}
pub async fn update_kernel_with_signals(
config: &Config,
signal_emitter: Option<SignalEmitter>,
transaction: &str,
) -> Result<KernelUpdateResponse> {
let executor = if let Some(emitter) = signal_emitter {
EpmExecutor::new(config.clone()).with_signal_emitter(emitter)
} else {
EpmExecutor::new(config.clone())
};
let output = if transaction.is_empty() {
executor.update_kernel(false).await?
} else {
executor.update_kernel_with_progress(transaction).await?
};
if !output.success {
return Err(EpmError::Execution(format!(
"Kernel update failed: {}",
output.stderr
)));
}
let info = parser::parse_kernel_update_output(&output.stdout).unwrap_or_default();
Ok(KernelUpdateResponse {
message: if info.up_to_date {
"Kernel is already up to date".into()
} else {
"Kernel updated".into()
},
info,
})
}
pub async fn check_update_kernel(config: &Config) -> Result<KernelUpdateResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.update_kernel(true).await?;
let info = parser::parse_kernel_update_output(&output.stdout)?;
Ok(KernelUpdateResponse {
message: if info.up_to_date {
"Kernel is already up to date".into()
} else {
format!("Kernel update available: {}", info.available_kernel)
},
info,
})
}
pub async fn check(config: &Config) -> Result<CheckSystemResponse> {
let executor = EpmExecutor::new(config.clone());
let output = executor.check().await?;
// Parse issues from output
let issues: Vec<String> = output
.stdout
.lines()
.filter(|l| l.contains("error") || l.contains("warning") || l.contains("problem"))
.map(|l| l.to_string())
.collect();
Ok(CheckSystemResponse {
message: if issues.is_empty() {
"System check passed".into()
} else {
format!("Found {} issues", issues.len())
},
ok: issues.is_empty() && output.success,
issues,
})
}
pub mod executor;
pub mod parser;
pub mod commands;
pub use executor::*;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum EpmError {
#[error("EPM execution failed: {0}")]
Execution(String),
#[error("Failed to parse EPM output: {0}")]
Parse(String),
#[error("Invalid argument: {0}")]
InvalidArgument(String),
#[error("Polkit authorization failed: {0}")]
Polkit(String),
#[error("D-Bus error: {0}")]
Dbus(#[from] zbus::Error),
#[error("D-Bus FDO error: {0}")]
DbusFdo(#[from] zbus::fdo::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON serialization error: {0}")]
Json(#[from] serde_json::Error),
#[error("Package not found: {0}")]
PackageNotFound(String),
#[error("Operation cancelled")]
Cancelled,
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Command not found: {0}")]
CommandNotFound(String),
}
impl From<EpmError> for zbus::fdo::Error {
fn from(err: EpmError) -> Self {
zbus::fdo::Error::Failed(err.to_string())
}
}
pub type Result<T> = std::result::Result<T, EpmError>;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info};
use clap::Parser;
use zbus::Connection;
mod cli;
mod config;
mod dbus;
mod eepm;
mod error;
mod polkit;
mod types;
use cli::{Cli, Commands, QueryCommands, ManageCommands, PlayQueryCommands, PlayManageCommands, OutputFormat, print_json_response, print_error};
use config::Config;
use dbus::{EpmServer, EpmClient, DBUS_NAME};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into()),
)
.init();
let cli = Cli::parse();
let format = if cli.json {
OutputFormat::Json
} else {
OutputFormat::Text
};
let show_progress = cli.progress && !cli.quiet && !cli.json;
match cli.command {
Commands::DbusSystem => {
run_dbus_system().await?;
}
cmd => {
run_client_command(cmd, format, show_progress).await?;
}
}
Ok(())
}
async fn run_dbus_system() -> anyhow::Result<()> {
info!("Starting EEPM D-Bus service...");
if !nix::unistd::Uid::effective().is_root() {
error!("EEPM D-Bus service requires root privileges for system bus");
std::process::exit(1);
}
let config = Config::new();
let config = Arc::new(RwLock::new(config));
let connection = Connection::system().await?;
let server = EpmServer::new(config);
server.register(&connection).await?;
connection.request_name(DBUS_NAME).await?;
info!("EEPM D-Bus service started successfully on {}", DBUS_NAME);
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())?;
tokio::select! {
_ = sigterm.recv() => info!("Received SIGTERM"),
_ = sigint.recv() => info!("Received SIGINT"),
}
info!("Shutting down EEPM D-Bus service");
Ok(())
}
async fn run_client_command(cmd: Commands, format: OutputFormat, show_progress: bool) -> anyhow::Result<()> {
let client = match EpmClient::new().await {
Ok(c) => c.with_progress(show_progress),
Err(e) => {
print_error(&format!("Failed to connect to D-Bus service: {}", e), format);
return Ok(());
}
};
let result = match cmd {
Commands::Query { cmd: q } => match q {
QueryCommands::Search { query, installed } => client.search(&query, installed).await,
QueryCommands::Info { package } => client.info(&package).await,
QueryCommands::List { limit, offset } => client.list(limit, offset).await,
QueryCommands::Requires { package } => client.requires(&package).await,
QueryCommands::WhatDepends { package } => client.whatdepends(&package).await,
QueryCommands::FileList { package } => client.filelist(&package).await,
QueryCommands::CheckInstall { packages } => client.check_install(&packages).await,
QueryCommands::CheckRemove { packages, depends } => client.check_remove(&packages, depends).await,
QueryCommands::CheckUpgrade => client.check_upgrade().await,
QueryCommands::CheckAutoRemove => client.check_autoremove().await,
QueryCommands::CheckAutoOrphans => client.check_autoorphans().await,
QueryCommands::CheckUpdateKernel => client.check_update_kernel().await,
QueryCommands::Check => client.check().await,
},
Commands::Manage { cmd: m } => match m {
ManageCommands::Install { packages } => client.install(&packages).await,
ManageCommands::Remove { packages, purge, depends } => client.remove(&packages, purge, depends).await,
ManageCommands::Download { packages } => client.download(&packages).await,
ManageCommands::Update => client.update().await,
ManageCommands::Upgrade => client.upgrade().await,
ManageCommands::Clean => client.clean().await,
ManageCommands::AutoRemove => client.autoremove().await,
ManageCommands::AutoOrphans => client.autoorphans().await,
ManageCommands::UpdateKernel => client.update_kernel().await,
},
Commands::PlayQuery { cmd: pq } => match pq {
PlayQueryCommands::List => client.play_list().await,
PlayQueryCommands::ListAll => client.play_list_all().await,
PlayQueryCommands::ListScripts => client.play_list_scripts().await,
PlayQueryCommands::Search { query } => client.play_search(&query).await,
PlayQueryCommands::Info { app } => client.play_info(&app).await,
PlayQueryCommands::Installed { app } => client.play_installed(&app).await,
PlayQueryCommands::AvailableVersion { app } => client.play_available_version(&app).await,
PlayQueryCommands::InstalledVersion { app } => client.play_installed_version(&app).await,
},
Commands::PlayManage { cmd: pm } => match pm {
PlayManageCommands::Install { app } => client.play_install(&app).await,
PlayManageCommands::Remove { app } => client.play_remove(&app).await,
PlayManageCommands::Update { app } => client.play_update(&app).await,
},
Commands::DbusSystem => unreachable!(),
};
match result {
Ok(json) => print_json_response(&json, format),
Err(e) => print_error(&e.to_string(), format),
}
Ok(())
}
use std::collections::HashMap;
use zbus::Connection;
use zbus::zvariant::Value;
use crate::error::{EpmError, Result};
const POLKIT_BUS_NAME: &str = "org.freedesktop.PolicyKit1";
const POLKIT_OBJECT_PATH: &str = "/org/freedesktop/PolicyKit1/Authority";
const POLKIT_INTERFACE: &str = "org.freedesktop.PolicyKit1.Authority";
/// Get the PID of the calling process via D-Bus
async fn get_caller_pid(conn: &Connection, sender: &str) -> Result<u32> {
let proxy = zbus::fdo::DBusProxy::new(conn).await?;
let sender_name: zbus::names::BusName = sender.try_into()
.map_err(|e| EpmError::Polkit(format!("Invalid sender name: {:?}", e)))?;
let pid = proxy.get_connection_unix_process_id(sender_name).await?;
Ok(pid)
}
fn get_start_time(pid: u32) -> Result<u64> {
let stat = std::fs::read_to_string(format!("/proc/{}/stat", pid))
.map_err(|e| EpmError::Polkit(format!("Cannot read /proc/{}/stat: {}", pid, e)))?;
// Process name (field 2) is in parentheses and may contain spaces/special chars.
// Find the last ')' to skip past the process name, then parse remaining fields.
let close_paren = stat
.rfind(')')
.ok_or_else(|| EpmError::Polkit("Invalid /proc/stat format".into()))?;
let after_name = &stat[close_paren + 1..];
let fields: Vec<&str> = after_name.split_whitespace().collect();
// After ')' fields are: state(0), ppid(1), pgrp(2), session(3), tty_nr(4),
// tpgid(5), flags(6), minflt(7), cminflt(8), majflt(9), cmajflt(10),
// utime(11), stime(12), cutime(13), cstime(14), priority(15), nice(16),
// num_threads(17), itrealvalue(18), starttime(19)
let start_time: u64 = fields
.get(19)
.ok_or_else(|| EpmError::Polkit("Cannot parse /proc stat".into()))?
.parse()
.map_err(|_| EpmError::Polkit("Cannot parse starttime".into()))?;
Ok(start_time)
}
/// Check authorization via Polkit
pub async fn check_authorization(
conn: &Connection,
sender: &str,
action_id: &str,
) -> Result<()> {
let pid = get_caller_pid(conn, sender).await?;
let start_time = get_start_time(pid)?;
let subject_kind = "unix-process";
// Helper to build subject details (Value doesn't impl Clone)
let build_subject = || {
let mut details: HashMap<&str, Value> = HashMap::new();
details.insert("pid", Value::U32(pid));
details.insert("start-time", Value::U64(start_time));
details
};
// Use direct method call via connection - first try without interaction
let msg = conn
.call_method(
Some(POLKIT_BUS_NAME),
POLKIT_OBJECT_PATH,
Some(POLKIT_INTERFACE),
"CheckAuthorization",
&(
(subject_kind, build_subject()),
action_id,
HashMap::<String, String>::new(),
0u32, // flags = 0 (no interaction)
"",
),
)
.await
.map_err(|e| EpmError::Polkit(format!("Polkit call failed: {}", e)))?;
let result: (bool, bool, HashMap<String, String>) = msg.body().deserialize()
.map_err(|e| EpmError::Polkit(format!("Failed to parse polkit response: {}", e)))?;
if result.0 {
return Ok(());
}
// Second try with user interaction (flags = 1)
let msg = conn
.call_method(
Some(POLKIT_BUS_NAME),
POLKIT_OBJECT_PATH,
Some(POLKIT_INTERFACE),
"CheckAuthorization",
&(
(subject_kind, build_subject()),
action_id,
HashMap::<String, String>::new(),
1u32, // flags = 1 (allow user interaction)
"",
),
)
.await
.map_err(|e| EpmError::Polkit(format!("Polkit call failed: {}", e)))?;
let result: (bool, bool, HashMap<String, String>) = msg.body().deserialize()
.map_err(|e| EpmError::Polkit(format!("Failed to parse polkit response: {}", e)))?;
if result.0 {
Ok(())
} else {
Err(EpmError::Polkit(format!(
"Not authorized by polkit (action={})",
action_id
)))
}
}
/// The Polkit action ID for managing packages
pub const ACTION_MANAGE: &str = "ru.etersoft.EPM.manage";
pub mod check;
pub use check::*;
pub mod package;
pub mod play;
pub mod response;
pub use package::*;
pub use play::*;
pub use response::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Package {
pub name: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub release: String,
#[serde(default)]
pub arch: String,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub size: Option<u64>,
#[serde(default)]
pub installed_size: Option<u64>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub license: Option<String>,
#[serde(default)]
pub group: Option<String>,
#[serde(default)]
pub repo: Option<String>,
#[serde(default)]
pub is_installed: bool,
#[serde(default)]
pub has_update: bool,
#[serde(default)]
pub installed_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PackageChanges {
#[serde(default)]
pub install: Vec<Package>,
#[serde(default)]
pub remove: Vec<Package>,
#[serde(default)]
pub upgrade: Vec<Package>,
#[serde(default)]
pub downgrade: Vec<Package>,
#[serde(default)]
pub installed_count: u32,
#[serde(default)]
pub removed_count: u32,
#[serde(default)]
pub upgraded_count: u32,
#[serde(default)]
pub download_size: u64,
#[serde(default)]
pub install_size: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageDependency {
pub name: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub relation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageFile {
pub path: String,
#[serde(default)]
pub file_type: Option<String>,
}
use serde::{Deserialize, Serialize};
/// Application from epm play
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PlayApp {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub installed: bool,
}
/// Detailed application info from epm play --info
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PlayAppInfo {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub help: Option<String>,
#[serde(default)]
pub installed: bool,
#[serde(default)]
pub available_version: Option<String>,
#[serde(default)]
pub installed_version: Option<String>,
}
/// Response for play list/list-all commands
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayListResponse {
pub message: String,
pub apps: Vec<PlayApp>,
pub total_count: u32,
}
/// Response for play search command
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaySearchResponse {
pub message: String,
pub apps: Vec<PlayApp>,
pub total_count: u32,
}
/// Response for play info command
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayInfoResponse {
pub message: String,
pub app: PlayAppInfo,
}
/// Response for play install/remove/update actions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayActionResponse {
pub message: String,
pub app: String,
pub transaction: String,
}
/// Response for play version queries
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayVersionResponse {
pub version: Option<String>,
pub installed: bool,
}
use serde::{Deserialize, Serialize};
use super::package::{Package, PackageChanges, PackageDependency, PackageFile};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiResponse<T> {
pub data: T,
#[serde(default)]
pub error: bool,
#[serde(default)]
pub message: Option<String>,
}
impl<T> ApiResponse<T> {
pub fn success(data: T) -> Self {
Self {
data,
error: false,
message: None,
}
}
#[allow(dead_code)]
pub fn with_message(data: T, message: impl Into<String>) -> Self {
Self {
data,
error: false,
message: Some(message.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstallRemoveResponse {
pub message: String,
pub info: PackageChanges,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckResponse {
pub message: String,
pub info: PackageChanges,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResponse {
pub message: String,
pub packages: Vec<Package>,
pub total_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InfoResponse {
pub message: String,
pub package: Package,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListResponse {
pub message: String,
pub packages: Vec<Package>,
pub total_count: u32,
pub offset: u32,
pub limit: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateResponse {
pub message: String,
pub count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpgradeResponse {
pub message: String,
pub info: PackageChanges,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CleanResponse {
pub message: String,
pub freed_space: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DownloadResponse {
pub message: String,
pub files: Vec<String>,
pub total_size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequiresResponse {
pub message: String,
pub dependencies: Vec<PackageDependency>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatDependsResponse {
pub message: String,
pub dependents: Vec<Package>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileListResponse {
pub message: String,
pub files: Vec<PackageFile>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackgroundTaskResponse {
pub message: String,
pub transaction: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckSystemResponse {
pub message: String,
pub ok: bool,
pub issues: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct KernelUpdateInfo {
/// Currently running kernel (e.g., "kernel-image-6.18-6.18.3-alt1")
pub running_kernel: String,
/// Available kernel to install (e.g., "kernel-image-rt-6.12.63-alt1")
pub available_kernel: String,
/// Kernel type (e.g., "6.18", "rt", "std")
pub kernel_type: String,
/// Kernel version (e.g., "6.12.63-alt1")
pub kernel_version: String,
/// Total number of external modules available for this kernel
pub total_modules: u32,
/// Modules available for the target kernel
pub available_modules: Vec<String>,
/// Modules that exist for current kernel but not for target
pub missing_modules: Vec<String>,
/// Auto-selected modules to install
pub auto_selected_modules: Vec<String>,
/// Attention/warning messages
pub attention: Vec<String>,
/// Package changes (install/upgrade/remove)
pub packages: PackageChanges,
/// Whether the latest kernel is already installed
pub up_to_date: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelUpdateResponse {
pub message: String,
pub info: KernelUpdateInfo,
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment