use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::os::unix::io::AsRawFd;
use color_eyre::Result;
use rust_apt::cache::PackageSort;
use crate::apt::{AptCache, format_apt_errors};
use crate::search::SearchIndex;
use crate::types::*;
#[derive(Default)]
pub struct SearchState {
pub index: Option<SearchIndex>,
pub query: String,
pub results: Option<HashSet<String>>,
}
#[derive(Clone)]
pub struct SortSettings {
pub sort_by: SortBy,
pub ascending: bool,
}
impl Default for SortSettings {
fn default() -> Self {
Self {
sort_by: SortBy::CandidateVersion,
ascending: true,
}
}
}
struct SharedState {
cache: AptCache,
user_intent: HashMap<PackageId, UserIntent>,
search: SearchState,
list: Vec<PackageInfo>,
filter_cache: HashMap<FilterCategory, (Vec<PackageInfo>, ColumnWidths)>,
upgradable_count: usize,
installed_count: usize,
total_count: usize,
selected_filter: FilterCategory,
sort_settings: SortSettings,
}
impl SharedState {
fn new(cache: AptCache) -> Self {
Self {
cache,
user_intent: HashMap::new(),
search: SearchState::default(),
list: Vec::new(),
filter_cache: HashMap::new(),
upgradable_count: 0,
installed_count: 0,
total_count: 0,
selected_filter: FilterCategory::Upgradable,
sort_settings: SortSettings::default(),
}
}
fn compute_cache_counts(&mut self) {
self.upgradable_count = 0;
self.installed_count = 0;
self.total_count = 0;
for pkg in self.cache.packages(&PackageSort::default()) {
self.total_count += 1;
if pkg.is_installed() {
self.installed_count += 1;
if pkg.is_upgradable() {
self.upgradable_count += 1;
}
}
}
}
}
pub struct PackageManager<S> {
shared: SharedState,
state: S,
}
impl PackageManager<Clean> {
pub fn new() -> Result<Self> {
let cache = AptCache::new()?;
let mut shared = SharedState::new(cache);
shared.compute_cache_counts();
let mut mgr = Self {
shared,
state: Clean,
};
mgr.rebuild_list();
Ok(mgr)
}
pub fn mark_install(mut self, id: PackageId) -> PackageManager<Dirty> {
self.shared.user_intent.insert(id, UserIntent::Install);
PackageManager {
shared: self.shared,
state: Dirty,
}
}
}
impl PackageManager<Dirty> {
pub fn mark_install(mut self, id: PackageId) -> Self {
self.shared.user_intent.insert(id, UserIntent::Install);
self
}
pub fn unmark(mut self, id: PackageId) -> Self {
self.shared.user_intent.remove(&id);
self
}
pub fn reset(mut self) -> PackageManager<Clean> {
self.shared.user_intent.clear();
self.shared
.cache
.clear_all_marks()
.expect("APT state corruption: failed to clear marks during reset");
PackageManager {
shared: self.shared,
state: Clean,
}
}
#[hotpath::measure]
pub fn plan(mut self) -> PackageManager<Planned> {
self.shared
.cache
.clear_all_marks()
.expect("APT state corruption: failed to clear marks during plan");
for (&id, &intent) in &self.shared.user_intent {
match intent {
UserIntent::Install => self.shared.cache.mark_install_id(id),
UserIntent::Remove => self.shared.cache.mark_delete_id(id),
UserIntent::Hold => self.shared.cache.mark_keep_id(id),
UserIntent::Default => {}
}
}
let errors = match self.shared.cache.resolve() {
Ok(()) => Vec::new(),
Err(e) => vec![format_apt_errors(&e)],
};
let change_data: Vec<_> = self.shared.cache.get_changes()
.map(|pkg| {
let fullname = pkg.fullname(false);
let is_installed = pkg.is_installed();
let marked_install = pkg.marked_install();
let marked_upgrade = pkg.marked_upgrade();
let marked_delete = pkg.marked_delete();
let candidate_info = pkg.candidate().map(|c| (c.size(), c.installed_size()));
(fullname, is_installed, marked_install, marked_upgrade, marked_delete, candidate_info)
})
.collect();
let mut changes = Vec::new();
let mut download_size = 0u64;
let mut install_size_change = 0i64;
for (fullname, is_installed, marked_install, marked_upgrade, marked_delete, candidate_info) in change_data {
let id = self.shared.cache.id_for(&fullname);
let is_user_requested = self.shared.user_intent.contains_key(&id);
let (action, reason) = if marked_install || marked_upgrade {
let action = if is_installed {
ChangeAction::Upgrade
} else {
ChangeAction::Install
};
let reason = if is_user_requested {
ChangeReason::UserRequested
} else {
ChangeReason::Dependency
};
(action, reason)
} else if marked_delete {
let reason = if is_user_requested {
ChangeReason::UserRequested
} else {
ChangeReason::AutoRemove
};
(ChangeAction::Remove, reason)
} else {
continue;
};
let (pkg_download, pkg_size_change) = if let Some((dl_size, inst_size)) = candidate_info {
let sz = if action == ChangeAction::Remove {
-(inst_size as i64)
} else {
inst_size as i64
};
(dl_size, sz)
} else {
(0, 0)
};
download_size += pkg_download;
install_size_change += pkg_size_change;
changes.push(PlannedChange {
package: id,
action,
reason,
download_size: pkg_download,
size_change: pkg_size_change,
});
}
let planned = Planned {
changes,
download_size,
install_size_change,
errors,
};
PackageManager {
shared: self.shared,
state: planned,
}
}
}
impl PackageManager<Planned> {
pub fn changes(&self) -> &[PlannedChange] {
&self.state.changes
}
pub fn apply_planned_statuses(&mut self) {
let change_map: HashMap<PackageId, ChangeAction> = self.state.changes
.iter()
.map(|c| (c.package, c.action))
.collect();
for pkg in &mut self.shared.list {
if let Some(&action) = change_map.get(&pkg.id) {
pkg.status = match action {
ChangeAction::Install => PackageStatus::MarkedForInstall,
ChangeAction::Upgrade => PackageStatus::MarkedForUpgrade,
ChangeAction::Remove => PackageStatus::MarkedForRemove,
ChangeAction::Downgrade => PackageStatus::MarkedForUpgrade,
};
}
}
}
pub fn modify(self) -> PackageManager<Dirty> {
PackageManager {
shared: self.shared,
state: Dirty,
}
}
pub fn commit_with_progress(
mut self,
acquire_progress: &mut rust_apt::progress::AcquireProgress,
install_progress: &mut rust_apt::progress::InstallProgress,
) -> Result<PackageManager<Clean>> {
self.shared.cache.commit_with_progress(acquire_progress, install_progress)?;
self.shared.user_intent.clear();
self.shared.search.index = None;
Ok(PackageManager {
shared: self.shared,
state: Clean,
})
}
}
impl<S: ReadableState> PackageManager<S> {
pub fn get_package(&self, index: usize) -> Option<&PackageInfo> {
self.shared.list.get(index)
}
pub fn package_count(&self) -> usize {
self.shared.list.len()
}
pub fn is_user_marked(&self, id: PackageId) -> bool {
self.shared.user_intent.contains_key(&id)
}
pub fn selected_filter(&self) -> FilterCategory {
self.shared.selected_filter
}
pub fn upgradable_count(&self) -> usize {
self.shared.upgradable_count
}
pub fn search_query(&self) -> &str {
&self.shared.search.query
}
pub fn search_result_count(&self) -> Option<usize> {
self.shared.search.results.as_ref().map(std::collections::HashSet::len)
}
pub fn apply_filter(&mut self, filter: FilterCategory) {
self.shared.selected_filter = filter;
self.rebuild_list();
}
#[hotpath::measure]
pub fn rebuild_list(&mut self) -> ColumnWidths {
let filter = self.shared.selected_filter;
let has_search = self.shared.search.results.is_some();
if !has_search {
if let Some((cached_list, col_widths)) = self.shared.filter_cache.get(&filter) {
self.shared.list = cached_list.clone();
let col_widths = col_widths.clone();
Self::apply_user_intent_overlay(&mut self.shared.list, &self.shared.user_intent);
self.sort_list();
return col_widths;
}
}
self.shared.list.clear();
let sort = if self.shared.selected_filter == FilterCategory::Upgradable {
PackageSort::default().upgradable()
} else {
PackageSort::default()
};
let matching_ids: Vec<PackageId> = {
let search_results = &self.shared.search.results;
let user_intent = &self.shared.user_intent;
let fullname_to_id = &self.shared.cache.fullname_to_id;
self.shared.cache.packages(&sort)
.filter(|pkg| {
let matches_category = match self.shared.selected_filter {
FilterCategory::Upgradable => pkg.is_upgradable(),
FilterCategory::MarkedChanges => {
let has_user_intent = fullname_to_id.get(&pkg.fullname(false))
.map(|id| user_intent.contains_key(id))
.unwrap_or(false);
has_user_intent || pkg.marked_install() || pkg.marked_upgrade() || pkg.marked_delete()
}
FilterCategory::Installed => pkg.is_installed(),
FilterCategory::NotInstalled => !pkg.is_installed(),
FilterCategory::All => true,
};
let matches_search = match search_results {
Some(results) => results.contains(pkg.name()),
None => true,
};
matches_category && matches_search
})
.filter_map(|pkg| fullname_to_id.get(&pkg.fullname(false)).copied())
.collect()
};
for id in matching_ids {
if let Some(info) = self.shared.cache.get_by_id(id).and_then(|pkg| self.shared.cache.extract_package_info(&pkg)) {
self.shared.list.push(info);
}
}
let mut col_widths = ColumnWidths::new();
for pkg in &self.shared.list {
let display_len = self.shared.cache.display_name(&pkg.name).len() as u16;
col_widths.name = col_widths.name.max(display_len);
col_widths.section = col_widths.section.max(pkg.section.len() as u16);
col_widths.installed = col_widths.installed.max(pkg.installed_version.len() as u16);
col_widths.candidate = col_widths.candidate.max(pkg.candidate_version.len() as u16);
}
if !has_search {
self.shared.filter_cache.insert(filter, (self.shared.list.clone(), col_widths.clone()));
}
Self::apply_user_intent_overlay(&mut self.shared.list, &self.shared.user_intent);
self.sort_list();
col_widths
}
fn apply_user_intent_overlay(list: &mut [PackageInfo], user_intent: &HashMap<PackageId, UserIntent>) {
for info in list.iter_mut() {
if let Some(&intent) = user_intent.get(&info.id) {
info.status = match intent {
UserIntent::Install => {
if info.status == PackageStatus::Upgradable {
PackageStatus::MarkedForUpgrade
} else {
PackageStatus::MarkedForInstall
}
}
UserIntent::Remove => PackageStatus::MarkedForRemove,
UserIntent::Hold => PackageStatus::Keep,
UserIntent::Default => info.status,
};
}
}
}
fn sort_list(&mut self) {
let sort_by = self.shared.sort_settings.sort_by;
let ascending = self.shared.sort_settings.ascending;
self.shared.list.sort_by(|a, b| {
let ord = match sort_by {
SortBy::Name => a.name.cmp(&b.name),
SortBy::Section => a.section.cmp(&b.section),
SortBy::InstalledVersion => a.installed_version.cmp(&b.installed_version),
SortBy::CandidateVersion => a.candidate_version.cmp(&b.candidate_version),
};
if ascending { ord } else { ord.reverse() }
});
}
pub fn set_sort(&mut self, sort_by: SortBy, ascending: bool) {
self.shared.sort_settings.sort_by = sort_by;
self.shared.sort_settings.ascending = ascending;
self.sort_list();
}
pub fn ensure_search_index(&mut self) -> Result<std::time::Duration> {
if self.shared.search.index.is_none() {
let mut index = SearchIndex::new()?;
let (_count, duration) = index.build(&self.shared.cache)?;
self.shared.search.index = Some(index);
return Ok(duration);
}
Ok(std::time::Duration::ZERO)
}
pub fn set_search_query(&mut self, query: &str) -> Result<()> {
self.shared.search.query = query.to_string();
if query.is_empty() {
self.shared.search.results = None;
} else if let Some(ref index) = self.shared.search.index {
self.shared.search.results = Some(index.search(query)?);
}
Ok(())
}
pub fn clear_search(&mut self) {
self.shared.search.query.clear();
self.shared.search.results = None;
}
pub fn get_dependencies(&self, name: &str) -> Vec<(String, String)> {
self.shared.cache.get_dependencies(name)
}
pub fn get_reverse_dependencies(&self, name: &str) -> Vec<(String, String)> {
self.shared.cache.get_reverse_dependencies(name)
}
}
#[derive(Default)]
pub enum ManagerState {
Clean(PackageManager<Clean>),
Dirty(PackageManager<Dirty>),
Planned(PackageManager<Planned>),
#[default]
Transitioning,
}
impl ManagerState {
fn shared(&self) -> &SharedState {
match self {
ManagerState::Clean(m) => &m.shared,
ManagerState::Dirty(m) => &m.shared,
ManagerState::Planned(m) => &m.shared,
ManagerState::Transitioning => panic!("Transitioning state observed"),
}
}
fn shared_mut(&mut self) -> &mut SharedState {
match self {
ManagerState::Clean(m) => &mut m.shared,
ManagerState::Dirty(m) => &mut m.shared,
ManagerState::Planned(m) => &mut m.shared,
ManagerState::Transitioning => panic!("Transitioning state observed"),
}
}
pub fn new() -> Result<Self> {
Ok(ManagerState::Clean(PackageManager::new()?))
}
pub fn planned_changes(&self) -> Option<&[PlannedChange]> {
match self {
ManagerState::Planned(m) => Some(m.changes()),
_ => None,
}
}
pub fn has_marks(&self) -> bool {
!self.shared().user_intent.is_empty()
}
pub fn user_mark_count(&self) -> usize {
self.shared().user_intent.len()
}
pub fn download_size(&self) -> u64 {
match self {
ManagerState::Planned(m) => m.state.download_size,
_ => 0,
}
}
pub fn install_size_change(&self) -> i64 {
match self {
ManagerState::Planned(m) => m.state.install_size_change,
_ => 0,
}
}
pub fn list(&self) -> &[PackageInfo] {
self.shared().list.as_slice()
}
pub fn get_package(&self, index: usize) -> Option<&PackageInfo> {
self.shared().list.get(index)
}
pub fn package_count(&self) -> usize {
self.shared().list.len()
}
pub fn is_user_marked(&self, id: PackageId) -> bool {
self.shared().user_intent.contains_key(&id)
}
pub fn selected_filter(&self) -> FilterCategory {
self.shared().selected_filter
}
pub fn upgradable_count(&self) -> usize {
self.shared().upgradable_count
}
pub fn search_query(&self) -> &str {
&self.shared().search.query
}
pub fn search_result_count(&self) -> Option<usize> {
self.shared().search.results.as_ref().map(std::collections::HashSet::len)
}
pub fn get_dependencies(&self, name: &str) -> Vec<(String, String)> {
self.shared().cache.get_dependencies(name)
}
pub fn get_reverse_dependencies(&self, name: &str) -> Vec<(String, String)> {
self.shared().cache.get_reverse_dependencies(name)
}
pub fn fetch_changelog(&self, name: &str) -> Result<Vec<String>, String> {
match std::process::Command::new("apt")
.args(["changelog", name])
.output()
{
Ok(output) => {
if output.status.success() {
let content = String::from_utf8_lossy(&output.stdout);
let lines: Vec<String> = content.lines().map(std::string::ToString::to_string).collect();
if lines.is_empty() {
Ok(vec!["No changelog available.".to_string()])
} else {
Ok(lines)
}
} else {
let err = String::from_utf8_lossy(&output.stderr);
Err(format!("Error: {err}"))
}
}
Err(e) => Err(format!("Failed to run apt changelog: {e}")),
}
}
pub fn apply_filter(&mut self, filter: FilterCategory) {
self.shared_mut().selected_filter = filter;
self.rebuild_list();
}
pub fn set_filter(&mut self, filter: FilterCategory) {
self.shared_mut().selected_filter = filter;
}
pub fn rebuild_list(&mut self) -> ColumnWidths {
match self {
ManagerState::Clean(m) => m.rebuild_list(),
ManagerState::Dirty(m) => m.rebuild_list(),
ManagerState::Planned(m) => {
let col_widths = m.rebuild_list();
m.apply_planned_statuses();
col_widths
}
ManagerState::Transitioning => panic!("Transitioning state observed"),
}
}
pub fn pre_warm_filter_cache(&mut self) {
let original_filter = self.selected_filter();
for &filter in FilterCategory::all() {
if filter == original_filter {
continue; }
self.set_filter(filter);
self.rebuild_list();
}
self.set_filter(original_filter);
self.rebuild_list();
}
pub fn set_sort(&mut self, sort_by: SortBy, ascending: bool) {
let shared = self.shared_mut();
shared.sort_settings.sort_by = sort_by;
shared.sort_settings.ascending = ascending;
let asc = shared.sort_settings.ascending;
shared.list.sort_by(|a, b| {
let ord = match sort_by {
SortBy::Name => a.name.cmp(&b.name),
SortBy::Section => a.section.cmp(&b.section),
SortBy::InstalledVersion => a.installed_version.cmp(&b.installed_version),
SortBy::CandidateVersion => a.candidate_version.cmp(&b.candidate_version),
};
if asc { ord } else { ord.reverse() }
});
}
pub fn ensure_search_index(&mut self) -> Result<std::time::Duration> {
let shared = self.shared_mut();
if shared.search.index.is_none() {
let mut index = SearchIndex::new()?;
let (_count, duration) = index.build(&shared.cache)?;
shared.search.index = Some(index);
return Ok(duration);
}
Ok(std::time::Duration::ZERO)
}
pub fn set_search_query(&mut self, query: &str) -> Result<()> {
let shared = self.shared_mut();
shared.search.query = query.to_string();
if query.is_empty() {
shared.search.results = None;
} else if let Some(ref index) = shared.search.index {
shared.search.results = Some(index.search(query)?);
}
Ok(())
}
pub fn clear_search(&mut self) {
let shared = self.shared_mut();
shared.search.query.clear();
shared.search.results = None;
}
pub fn has_search_results(&self) -> bool {
self.shared().search.results.is_some()
}
pub fn search_query_pop(&mut self) {
self.shared_mut().search.query.pop();
}
pub fn search_query_push(&mut self, c: char) {
self.shared_mut().search.query.push(c);
}
pub fn refresh(&mut self) -> Result<(), String> {
let shared = self.shared_mut();
shared.cache.refresh().map_err(|e| e.to_string())?;
shared.user_intent.clear();
shared.filter_cache.clear();
shared.search.index = None;
shared.search.query.clear();
shared.search.results = None;
shared.compute_cache_counts();
self.reset();
Ok(())
}
pub fn update_cache_counts(&mut self) {
self.shared_mut().compute_cache_counts();
}
pub fn filter_count(&self, filter: FilterCategory) -> usize {
let shared = self.shared();
let (upgradable, installed, total, user_marks) =
(shared.upgradable_count, shared.installed_count, shared.total_count, shared.user_intent.len());
match filter {
FilterCategory::Upgradable => upgradable,
FilterCategory::MarkedChanges => {
self.planned_changes()
.map(|changes| changes.len())
.unwrap_or(user_marks)
}
FilterCategory::Installed => installed,
FilterCategory::NotInstalled => total - installed,
FilterCategory::All => total,
}
}
pub fn mark_all_upgradable(&mut self) {
let upgradable_ids: Vec<PackageId> = {
let cache = self.cache();
cache.packages(&PackageSort::default().upgradable())
.map(|pkg| pkg.fullname(false))
.filter_map(|name| cache.get_id(&name))
.collect()
};
for id in upgradable_ids {
self.mark_install(id);
}
}
pub fn cache(&self) -> &AptCache {
&self.shared().cache
}
}
impl ManagerState {
pub fn mark_install(&mut self, id: PackageId) {
*self = match std::mem::take(self) {
ManagerState::Clean(m) => ManagerState::Dirty(m.mark_install(id)),
ManagerState::Dirty(m) => ManagerState::Dirty(m.mark_install(id)),
ManagerState::Planned(m) => ManagerState::Dirty(m.modify().mark_install(id)),
ManagerState::Transitioning => panic!("ManagerState::Transitioning should not be observed"),
};
}
pub fn unmark(&mut self, id: PackageId) {
if !self.is_user_marked(id) {
return;
}
*self = match std::mem::take(self) {
ManagerState::Clean(m) => ManagerState::Clean(m),
ManagerState::Dirty(m) => ManagerState::Dirty(m.unmark(id)),
ManagerState::Planned(m) => ManagerState::Dirty(m.modify().unmark(id)),
ManagerState::Transitioning => panic!("ManagerState::Transitioning should not be observed"),
};
}
#[hotpath::measure]
pub fn toggle(&mut self, id: PackageId) -> ToggleResult {
self.compute_plan();
let is_currently_marked = self.planned_changes()
.is_some_and(|changes| changes.iter().any(|c| c.package == id));
if is_currently_marked {
self.toggle_unmark(id)
} else {
self.toggle_mark_impl(id)
}
}
fn toggle_mark_impl(&mut self, id: PackageId) -> ToggleResult {
let planned_before: HashSet<PackageId> = self.planned_changes()
.map(|changes| changes.iter().map(|c| c.package).collect())
.unwrap_or_default();
self.mark_install(id);
self.compute_plan();
self.rebuild_list();
let newly_marked: Vec<PackageId> = self.planned_changes()
.map(|changes| {
changes.iter()
.filter(|c| c.package != id && !planned_before.contains(&c.package))
.map(|c| c.package)
.collect()
})
.unwrap_or_default();
ToggleResult::Marked {
package: id,
additional: newly_marked,
}
}
fn toggle_unmark(&mut self, id: PackageId) -> ToggleResult {
let planned_before: HashSet<PackageId> = self.planned_changes()
.map(|changes| changes.iter().map(|c| c.package).collect())
.unwrap_or_default();
let to_remove: Vec<PackageId> = if self.is_user_marked(id) {
vec![id]
} else {
self.find_user_intent_depending_on(id)
};
for pkg_id in &to_remove {
self.unmark(*pkg_id);
}
self.compute_plan();
self.rebuild_list();
let planned_after: HashSet<PackageId> = self.planned_changes()
.map(|changes| changes.iter().map(|c| c.package).collect())
.unwrap_or_default();
if planned_after.contains(&id) {
return ToggleResult::NoChange { package: id };
}
let also_unmarked: Vec<PackageId> = planned_before.iter()
.filter(|pkg_id| !planned_after.contains(pkg_id) && **pkg_id != id)
.copied()
.collect();
ToggleResult::Unmarked {
package: id,
also_unmarked,
}
}
fn find_user_intent_depending_on(&self, target_id: PackageId) -> Vec<PackageId> {
let cache = self.cache();
let target_name = match cache.fullname_of(target_id) {
Some(n) => n,
None => return Vec::new(),
};
let target_base = target_name.split(':').next().unwrap_or(target_name);
let mut result = Vec::new();
let intent_ids: Vec<PackageId> = self.user_intent_ids().copied().collect();
for intent_id in intent_ids {
if let Some(intent_name) = cache.fullname_of(intent_id)
&& self.package_depends_on(intent_name, target_base) {
result.push(intent_id);
}
}
result
}
fn package_depends_on(&self, pkg_name: &str, target_base: &str) -> bool {
let cache = self.cache();
let mut visited = HashSet::new();
let mut to_check = vec![pkg_name.to_string()];
while let Some(current) = to_check.pop() {
if visited.contains(¤t) {
continue;
}
visited.insert(current.clone());
let deps = cache.get_dependencies(¤t);
for (dep_type, dep_name) in deps {
if dep_type != "Depends" && dep_type != "PreDepends" {
continue;
}
if dep_name == target_base {
return true;
}
if let Some(&dep_id) = cache.fullname_to_id.get(&dep_name)
.or_else(|| cache.fullname_to_id.get(&format!("{}:{}", dep_name, cache.native_arch())))
&& let Some(fullname) = cache.fullname_of(dep_id) {
to_check.push(fullname.to_string());
}
}
}
false
}
fn user_intent_ids(&self) -> impl Iterator<Item = &PackageId> {
self.shared().user_intent.keys()
}
pub fn reset(&mut self) {
*self = match std::mem::take(self) {
ManagerState::Clean(m) => ManagerState::Clean(m),
ManagerState::Dirty(m) => ManagerState::Clean(m.reset()),
ManagerState::Planned(m) => ManagerState::Clean(m.modify().reset()),
ManagerState::Transitioning => panic!("ManagerState::Transitioning should not be observed"),
};
}
pub fn compute_plan(&mut self) {
*self = match std::mem::take(self) {
ManagerState::Clean(m) => ManagerState::Clean(m), ManagerState::Dirty(m) => {
let planned = m.plan();
ManagerState::Planned(planned)
}
ManagerState::Planned(m) => ManagerState::Planned(m), ManagerState::Transitioning => panic!("ManagerState::Transitioning should not be observed"),
};
self.invalidate_filter_cache(Some(FilterCategory::MarkedChanges));
}
fn invalidate_filter_cache(&mut self, filter: Option<FilterCategory>) {
if matches!(self, ManagerState::Transitioning) {
return;
}
let shared = self.shared_mut();
match filter {
Some(f) => { shared.filter_cache.remove(&f); }
None => shared.filter_cache.clear(),
}
}
pub fn commit_with_progress(
&mut self,
acquire_progress: &mut rust_apt::progress::AcquireProgress,
install_progress: &mut rust_apt::progress::InstallProgress,
) -> Result<()> {
let taken = std::mem::take(self);
let result = match taken {
ManagerState::Clean(m) => {
*self = ManagerState::Clean(m);
return Ok(());
}
ManagerState::Dirty(m) => {
let planned = m.plan();
planned.commit_with_progress(acquire_progress, install_progress)
}
ManagerState::Planned(m) => {
m.commit_with_progress(acquire_progress, install_progress)
}
ManagerState::Transitioning => panic!("ManagerState::Transitioning should not be observed"),
};
match result {
Ok(clean) => {
*self = ManagerState::Clean(clean);
Ok(())
}
Err(e) => {
match ManagerState::new() {
Ok(fresh) => *self = fresh,
Err(reinit_err) => {
return Err(e.wrap_err(format!(
"additionally, failed to reinitialize package cache: {reinit_err}"
)));
}
}
Err(e)
}
}
}
pub fn update_with_progress(
&mut self,
acquire_progress: &mut rust_apt::progress::AcquireProgress,
) -> Result<(), String> {
let shared = self.shared_mut();
shared.cache.update_with_progress(acquire_progress)
.map_err(|e| e.to_string())?;
shared.user_intent.clear();
shared.filter_cache.clear();
shared.search.index = None;
shared.search.query.clear();
shared.search.results = None;
shared.compute_cache_counts();
self.reset();
Ok(())
}
pub fn build_mark_preview(
&self,
marked_pkg_id: PackageId,
previously_planned: &HashSet<PackageId>,
) -> Option<MarkPreview> {
let changes = self.planned_changes()?;
let cache = self.cache();
let marked_pkg_name = cache.fullname_of(marked_pkg_id)
.map(|n| cache.display_name(n).to_string())?;
let mut additional_installs = Vec::new();
let mut additional_upgrades = Vec::new();
let mut additional_removes = Vec::new();
let mut download_size = 0u64;
let mut is_upgrade = false;
for change in changes {
if change.package == marked_pkg_id {
download_size += change.download_size;
is_upgrade = change.action == ChangeAction::Upgrade;
continue;
}
if previously_planned.contains(&change.package) {
continue;
}
download_size += change.download_size;
let name = cache.fullname_of(change.package)
.map(|n| cache.display_name(n).to_string())
.unwrap_or_else(|| format!("(unknown:{})", change.package.index()));
match change.action {
ChangeAction::Install => additional_installs.push(name),
ChangeAction::Upgrade => additional_upgrades.push(name),
ChangeAction::Remove => additional_removes.push(name),
ChangeAction::Downgrade => additional_upgrades.push(name),
}
}
Some(MarkPreview::Mark {
package_name: marked_pkg_name,
is_upgrade,
additional_installs,
additional_upgrades,
additional_removes,
download_size,
bulk_acted_ids: Vec::new(),
})
}
}
pub fn is_root() -> bool {
unsafe { libc::geteuid() == 0 }
}
pub fn check_apt_lock() -> Option<String> {
let lock_paths = [
"/var/lib/dpkg/lock-frontend",
"/var/lib/dpkg/lock",
"/var/lib/apt/lists/lock",
];
for path in &lock_paths {
if let Ok(file) = File::open(path) {
let fd = file.as_raw_fd();
let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
if ret != 0 {
return Some(format!(
"Another package manager is running ({path}). Close it and try again."
));
}
unsafe { libc::flock(fd, libc::LOCK_UN) };
}
}
None
}