use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::sync::Arc;
use itertools::Itertools;
use mcvm_core::net::download::get_transfer_limit;
use mcvm_pkg::properties::PackageProperties;
use mcvm_pkg::repo::PackageFlag;
use mcvm_pkg::PkgRequest;
use mcvm_shared::output::{MCVMOutput, MessageContents, MessageLevel};
use mcvm_shared::pkg::{ArcPkgReq, PackageID};
use mcvm_shared::translate;
use mcvm_shared::versions::VersionInfo;
use tokio::sync::Semaphore;
use tokio::task::JoinSet;
use crate::instance::Instance;
use crate::pkg::eval::{resolve, EvalConstants, EvalInput, EvalParameters};
use crate::util::select_random_n_items_from_list;
use mcvm_shared::id::InstanceID;
use super::InstanceUpdateContext;
use anyhow::Context;
pub async fn update_instance_packages<'a, O: MCVMOutput>(
instances: &mut [&mut Instance],
constants: &EvalConstants,
ctx: &mut InstanceUpdateContext<'a, O>,
force: bool,
) -> anyhow::Result<HashSet<ArcPkgReq>> {
ctx.output.start_process();
ctx.output.display(
MessageContents::StartProcess(translate!(ctx.output, StartResolvingDependencies)),
MessageLevel::Important,
);
let resolved_packages = resolve_and_batch(instances, constants, ctx)
.await
.context("Failed to resolve dependencies for profile")?;
ctx.output.display(
MessageContents::Success(translate!(ctx.output, FinishResolvingDependencies)),
MessageLevel::Important,
);
ctx.output.end_process();
ctx.output.display(
MessageContents::StartProcess(translate!(ctx.output, StartAcquiringAddons)),
MessageLevel::Important,
);
let mut tasks = HashMap::new();
let mut evals = HashMap::new();
for (package, package_instances) in resolved_packages
.package_to_instances
.iter()
.sorted_by_key(|x| x.0)
{
check_package(ctx, package)
.await
.with_context(|| format!("Failed to check package {package}"))?;
let mut notices = Vec::new();
for instance_id in package_instances {
let instance = instances
.iter_mut()
.find(|x| &x.id == instance_id)
.expect("Instance should exist");
let mut params = EvalParameters::new(instance.kind.to_side());
params.stability = instance.config.package_stability;
if let Some(config) = instance.get_package_config(&package.to_string()) {
params
.apply_config(config, &PackageProperties::default())
.context("Failed to apply config")?;
}
let input = EvalInput { constants, params };
let (eval, new_tasks) = instance
.get_package_addon_tasks(
package,
input,
ctx.packages,
ctx.paths,
force,
ctx.client,
ctx.plugins,
ctx.output,
)
.await
.with_context(|| {
format!("Failed to get addon install tasks for package '{package}' on instance")
})?;
tasks.extend(new_tasks);
notices.extend(
eval.notices
.iter()
.map(|x| (instance_id.clone(), x.to_owned())),
);
evals.insert((package, instance_id), eval);
}
for (instance, notice) in notices {
ctx.output.display(
format_package_update_message(
package,
Some(&instance),
MessageContents::Notice(notice),
),
MessageLevel::Important,
);
}
}
run_addon_tasks(tasks, ctx.output)
.await
.context("Failed to acquire addons")?;
ctx.output.display(
MessageContents::Success(translate!(ctx.output, FinishAcquiringAddons)),
MessageLevel::Important,
);
ctx.output.display(
MessageContents::StartProcess(translate!(ctx.output, StartInstallingPackages)),
MessageLevel::Important,
);
for (package, package_instances) in resolved_packages
.package_to_instances
.iter()
.sorted_by_key(|x| x.0)
{
ctx.output.start_process();
for instance_id in package_instances {
let instance = instances
.iter_mut()
.find(|x| &x.id == instance_id)
.expect("Instance should exist");
let version_info = VersionInfo {
version: constants.version.clone(),
versions: constants.version_list.clone(),
};
let eval = evals
.get(&(package, instance_id))
.expect("Evaluation should be in map");
instance
.install_eval_data(
package,
eval,
&version_info,
ctx.paths,
ctx.lock,
ctx.output,
)
.await
.context("Failed to install package on instance")?;
}
ctx.output.display(
format_package_update_message(
package,
None,
MessageContents::Success(translate!(ctx.output, FinishInstallingPackage)),
),
MessageLevel::Important,
);
ctx.output.end_process();
}
for (instance_id, packages) in resolved_packages.instance_to_packages {
let instance = instances
.iter()
.find(|x| x.id == instance_id)
.expect("Instance should exist");
let files_to_remove = ctx
.lock
.remove_unused_packages(
&instance_id,
&packages
.iter()
.map(|x| x.id.clone())
.collect::<Vec<PackageID>>(),
)
.context("Failed to remove unused packages")?;
for file in files_to_remove {
instance
.remove_addon_file(&file, ctx.paths)
.with_context(|| {
format!(
"Failed to remove addon file {} for instance {}",
file.display(),
instance_id
)
})?;
}
}
let mut out = HashSet::new();
out.extend(resolved_packages.package_to_instances.keys().cloned());
Ok(out)
}
async fn run_addon_tasks(
tasks: HashMap<String, impl Future<Output = anyhow::Result<()>> + Send + 'static>,
o: &mut impl MCVMOutput,
) -> anyhow::Result<()> {
let total_count = tasks.len();
let mut task_set = JoinSet::new();
let sem = Arc::new(Semaphore::new(get_transfer_limit()));
for task in tasks.into_values() {
let permit = sem.clone().acquire_owned().await;
let task = async move {
let _permit = permit?;
task.await
};
task_set.spawn(task);
}
o.start_process();
while let Some(result) = task_set.join_next().await {
result
.context("Failed to run addon acquire task")?
.context("Failed to acquire addon")?;
let progress = MessageContents::Progress {
current: (total_count - task_set.len()) as u32,
total: total_count as u32,
};
o.display(progress, MessageLevel::Important);
}
o.end_process();
Ok(())
}
async fn resolve_and_batch<'a, O: MCVMOutput>(
instances: &[&mut Instance],
constants: &EvalConstants,
ctx: &mut InstanceUpdateContext<'a, O>,
) -> anyhow::Result<ResolvedPackages> {
let mut batched: HashMap<ArcPkgReq, Vec<InstanceID>> = HashMap::new();
let mut resolved = HashMap::new();
for instance in instances {
let mut params = EvalParameters::new(instance.kind.to_side());
params.stability = instance.config.package_stability;
let instance_pkgs = instance.get_configured_packages();
let instance_resolved = resolve(
instance_pkgs,
constants,
params,
ctx.paths,
ctx.packages,
ctx.client,
ctx.plugins,
ctx.output,
)
.await
.with_context(|| {
format!(
"Failed to resolve package dependencies for instance '{}'",
instance.id
)
})?;
for package in &instance_resolved.packages {
if let Some(entry) = batched.get_mut(package) {
entry.push(instance.id.clone());
} else {
batched.insert(package.clone(), vec![instance.id.clone()]);
}
}
resolved.insert(instance.id.clone(), instance_resolved.packages);
}
Ok(ResolvedPackages {
package_to_instances: batched,
instance_to_packages: resolved,
})
}
struct ResolvedPackages {
pub package_to_instances: HashMap<ArcPkgReq, Vec<InstanceID>>,
pub instance_to_packages: HashMap<InstanceID, Vec<ArcPkgReq>>,
}
async fn check_package<'a, O: MCVMOutput>(
ctx: &mut InstanceUpdateContext<'a, O>,
pkg: &ArcPkgReq,
) -> anyhow::Result<()> {
let flags = ctx
.packages
.flags(pkg, ctx.paths, ctx.client, ctx.output)
.await
.context("Failed to get flags for package")?;
if flags.contains(&PackageFlag::OutOfDate) {
ctx.output.display(
MessageContents::Warning(translate!(ctx.output, PackageOutOfDate, "pkg" = &pkg.id)),
MessageLevel::Important,
);
}
if flags.contains(&PackageFlag::Deprecated) {
ctx.output.display(
MessageContents::Warning(translate!(ctx.output, PackageDeprecated, "pkg" = &pkg.id)),
MessageLevel::Important,
);
}
if flags.contains(&PackageFlag::Insecure) {
ctx.output.display(
MessageContents::Error(translate!(ctx.output, PackageInsecure, "pkg" = &pkg.id)),
MessageLevel::Important,
);
}
if flags.contains(&PackageFlag::Malicious) {
ctx.output.display(
MessageContents::Error(translate!(ctx.output, PackageMalicious, "pkg" = &pkg.id)),
MessageLevel::Important,
);
}
Ok(())
}
pub async fn print_package_support_messages<'a, O: MCVMOutput>(
packages: &[ArcPkgReq],
ctx: &mut InstanceUpdateContext<'a, O>,
) -> anyhow::Result<()> {
let package_count = 5;
let packages = select_random_n_items_from_list(packages, package_count);
let mut links = Vec::new();
for package in packages {
if let Some(link) = ctx
.packages
.get_metadata(package, ctx.paths, ctx.client, ctx.output)
.await?
.support_link
.clone()
{
links.push((package, link))
}
}
if !links.is_empty() {
ctx.output.display(
MessageContents::Header(translate!(ctx.output, PackageSupportHeader)),
MessageLevel::Important,
);
for (req, link) in links {
let msg = format_package_update_message(req, None, MessageContents::Hyperlink(link));
ctx.output.display(msg, MessageLevel::Important);
}
}
Ok(())
}
fn format_package_update_message(
pkg: &PkgRequest,
instance: Option<&str>,
message: MessageContents,
) -> MessageContents {
let msg = if let Some(instance) = instance {
MessageContents::Package(
pkg.to_owned(),
Box::new(MessageContents::Associated(
Box::new(MessageContents::Simple(instance.to_string())),
Box::new(message),
)),
)
} else {
MessageContents::Package(pkg.to_owned(), Box::new(message))
};
MessageContents::ListItem(Box::new(msg))
}