diff --git a/dnf5-plugins/CMakeLists.txt b/dnf5-plugins/CMakeLists.txt index 433002674..ce8504276 100644 --- a/dnf5-plugins/CMakeLists.txt +++ b/dnf5-plugins/CMakeLists.txt @@ -13,3 +13,4 @@ add_subdirectory("config-manager_plugin") add_subdirectory("copr_plugin") add_subdirectory("needs_restarting_plugin") add_subdirectory("repoclosure_plugin") +add_subdirectory("system_upgrade_plugin") diff --git a/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp b/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp index 0ea4cbeb0..d9e806332 100644 --- a/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp +++ b/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp @@ -46,4 +46,4 @@ class NeedsRestartingCommand : public Command { } // namespace dnf5 -#endif // DNF5_COMMANDS_CHANGELOG_HPP +#endif // DNF5_COMMANDS_NEEDS_RESTARTING_HPP diff --git a/dnf5-plugins/system_upgrade_plugin/CMakeLists.txt b/dnf5-plugins/system_upgrade_plugin/CMakeLists.txt new file mode 100644 index 000000000..bd6519526 --- /dev/null +++ b/dnf5-plugins/system_upgrade_plugin/CMakeLists.txt @@ -0,0 +1,22 @@ +set(SYSTEMD_UNIT_DIR /usr/lib/systemd/system) + +# set gettext domain for translations +add_definitions(-DGETTEXT_DOMAIN=\"dnf5_cmd_system_upgrade\") + +file(GLOB SYSTEM_UPGRADE_SOURCES *.cpp) +add_library(system_upgrade_cmd_plugin MODULE ${SYSTEM_UPGRADE_SOURCES}) + +# disable the 'lib' prefix in order to create system_upgrade_cmd_plugin.so +set_target_properties(system_upgrade_cmd_plugin PROPERTIES PREFIX "") + +pkg_check_modules(SDBUS_CPP REQUIRED sdbus-c++) +pkg_check_modules(LIBSYSTEMD REQUIRED libsystemd) + +target_link_libraries(system_upgrade_cmd_plugin PRIVATE dnf5 libdnf5 libdnf5-cli ${SDBUS_CPP_LIBRARIES} ${LIBSYSTEMD_LIBRARIES}) + +install(FILES "dnf5-system-upgrade.service" DESTINATION ${SYSTEMD_UNIT_DIR}) +install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink + ${SYSTEMD_UNIT_DIR}/dnf5-system-upgrade.service + ${SYSTEMD_UNIT_DIR}/system-update.target.wants/dnf5-system-upgrade.service)" +) +install(TARGETS system_upgrade_cmd_plugin LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}/dnf5/plugins/) diff --git a/dnf5-plugins/system_upgrade_plugin/dnf5-system-upgrade.service b/dnf5-plugins/system_upgrade_plugin/dnf5-system-upgrade.service new file mode 100644 index 000000000..a846ad5bf --- /dev/null +++ b/dnf5-plugins/system_upgrade_plugin/dnf5-system-upgrade.service @@ -0,0 +1,19 @@ +[Unit] +Description=System Upgrade using DNF 5 +ConditionPathExists=/system-update +Documentation=https://www.freedesktop.org/wiki/Software/systemd/SystemUpdates + +DefaultDependencies=no +Requires=sysinit.target +After=sysinit.target systemd-journald.socket system-update-pre.target +Before=poweroff.target reboot.target shutdown.target system-update.target + +[Service] +# We are done when the script exits, not before +Type=oneshot +# Upgrade output goes to journal and on-screen. +StandardOutput=journal+console +ExecStart=/usr/bin/dnf5 system-upgrade upgrade + +[Install] +WantedBy=system-update.target diff --git a/dnf5-plugins/system_upgrade_plugin/system_upgrade.cpp b/dnf5-plugins/system_upgrade_plugin/system_upgrade.cpp new file mode 100644 index 000000000..95c6bc62e --- /dev/null +++ b/dnf5-plugins/system_upgrade_plugin/system_upgrade.cpp @@ -0,0 +1,672 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with libdnf. If not, see . +*/ + +#include "system_upgrade.hpp" + +#include "utils/string.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace libdnf5::cli; + +const std::string & ID_TO_IDENTIFY_BOOTS = OFFLINE_STARTED_ID; + +OfflineTransactionState::OfflineTransactionState(std::filesystem::path path) : path(std::move(path)) { + read(); +} +void OfflineTransactionState::read() { + try { + const auto & value = toml::parse(path); + data = toml::find(value, STATE_HEADER); + if (data.state_version != STATE_VERSION) { + throw libdnf5::RuntimeError(M_("incompatible version of state data")); + } + } catch (const std::exception & ex) { + read_exception = std::current_exception(); + data = OfflineTransactionStateData{}; + } +} +void OfflineTransactionState::write() { + auto file = libdnf5::utils::fs::File(path, "w"); + file.write(toml::format(toml::value{{STATE_HEADER, data}})); + file.close(); +} + +int call(const std::string & command, const std::vector & args) { + std::vector c_args; + c_args.emplace_back(const_cast(command.c_str())); + for (const auto & arg : args) { + c_args.emplace_back(const_cast(arg.c_str())); + } + c_args.emplace_back(nullptr); + + const auto pid = fork(); + if (pid == -1) { + return -1; + } + if (pid == 0) { + int rc = execvp(command.c_str(), c_args.data()); + exit(rc == 0 ? 0 : -1); + } else { + int status; + int rc = waitpid(pid, &status, 0); + if (rc == -1) { + return -1; + } + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } + if (WIFSIGNALED(status)) { + return 128 + WTERMSIG(status); + } + return -1; + } +} + +namespace dnf5 { + +/// Helper for displaying messages with Plymouth +/// +/// Derived from DNF 4 system-upgrade PlymouthOutput implementation. Filters +/// duplicate calls, and stops calling the plymouth binary if we fail to +/// contact it. +class PlymouthOutput { +public: + bool ping() { return plymouth({"ping"}); } + bool set_mode() { return plymouth({"change-mode", "--system-upgrade"}); } + bool message(const std::string & message) { + if (last_message.has_value() && message == last_message) { + plymouth({"hide-message", "--text", last_message.value()}); + } + last_message = message; + return plymouth({"display-message", "--text", message}); + } + bool progress(const int percentage) { + return plymouth({"system-update", "--progress", std::to_string(percentage)}); + } + +private: + bool alive = true; + std::map> last_subcommand_args; + std::optional last_message; + bool plymouth(const std::vector & args) { + const auto & command = args.at(0); + const auto & last_args = last_subcommand_args.find(command); + + bool dupe_cmd = (last_args != last_subcommand_args.end() && args == last_args->second); + if ((alive && !dupe_cmd) || command == "--ping") { + alive = call(PATH_TO_PLYMOUTH, args) == 0; + last_subcommand_args[command] = args; + } + return alive; + } +}; + +/// Extend RpmTransCB to also display messages with Plymouth +class PlymouthTransCB : public RpmTransCB { +public: + PlymouthTransCB(PlymouthOutput plymouth) : plymouth(std::move(plymouth)) {} + void elem_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) override { + RpmTransCB::elem_progress(item, amount, total); + + plymouth.progress(static_cast(100 * static_cast(amount) / static_cast(total))); + + std::string action; + switch (item.get_action()) { + case libdnf5::transaction::TransactionItemAction::UPGRADE: + action = "Upgrading"; + break; + case libdnf5::transaction::TransactionItemAction::DOWNGRADE: + action = "Downgrading"; + break; + case libdnf5::transaction::TransactionItemAction::REINSTALL: + action = "Reinstalling"; + break; + case libdnf5::transaction::TransactionItemAction::INSTALL: + action = "Installing"; + break; + case libdnf5::transaction::TransactionItemAction::REMOVE: + action = "Removing"; + break; + case libdnf5::transaction::TransactionItemAction::REPLACED: + action = "Replacing"; + break; + case libdnf5::transaction::TransactionItemAction::REASON_CHANGE: + case libdnf5::transaction::TransactionItemAction::ENABLE: + case libdnf5::transaction::TransactionItemAction::DISABLE: + case libdnf5::transaction::TransactionItemAction::RESET: + throw std::logic_error(fmt::format( + "Unexpected action in TransactionPackage: {}", + static_cast>( + item.get_action()))); + break; + } + const auto & message = fmt::format("[{}/{}] {} {}...", amount, total, action, item.get_package().get_name()); + plymouth.message(message); + } + +private: + PlymouthOutput plymouth; +}; + +void SystemUpgradeCommand::pre_configure() { + throw_missing_command(); +} + +void SystemUpgradeCommand::set_parent_command() { + auto * arg_parser_parent_cmd = get_session().get_argument_parser().get_root_command(); + auto * arg_parser_this_cmd = get_argument_parser_command(); + arg_parser_parent_cmd->register_command(arg_parser_this_cmd); + arg_parser_parent_cmd->get_group("subcommands").register_argument(arg_parser_this_cmd); +} + +void SystemUpgradeCommand::set_argument_parser() { + get_argument_parser_command()->set_description("Prepare system for upgrade to a new release"); +} + +void SystemUpgradeCommand::register_subcommands() { + register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); +} + +SystemUpgradeSubcommand::SystemUpgradeSubcommand(Context & context, const std::string & name) + : Command(context, name), + datadir(std::filesystem::path{libdnf5::SYSTEM_STATE_DIR} / "system-upgrade"), + state(datadir / "state.toml") {} + +void SystemUpgradeSubcommand::set_argument_parser() { + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + auto & cmd = *get_argument_parser_command(); + + cachedir = + dynamic_cast(parser.add_init_value(std::make_unique(datadir))); + + auto * download_dir_arg = parser.add_new_named_arg("downloaddir"); + download_dir_arg->set_long_name("downloaddir"); + download_dir_arg->set_description("Redirect download of packages to provided "); + download_dir_arg->link_value(cachedir); + cmd.register_named_arg(download_dir_arg); +} + +void SystemUpgradeSubcommand::configure() { + auto & ctx = get_context(); + const std::filesystem::path installroot{ctx.base.get_config().get_installroot_option().get_value()}; + magic_symlink = installroot / "system-update"; + + system_releasever = *libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot); + target_releasever = ctx.base.get_vars()->get_value("releasever"); +} + +void SystemUpgradeSubcommand::log_status(const std::string & message, const std::string & message_id) const { + const auto & version = get_application_version(); + const std::string & version_string = fmt::format("{}.{}.{}", version.major, version.minor, version.micro); + sd_journal_send( + "MESSAGE=%s", + message.c_str(), + "MESSAGE_ID=%s", + message_id.c_str(), + "SYSTEM_RELEASEVER=%s", + get_system_releasever().c_str(), + "TARGET_RELEASEVER=%s", + get_target_releasever().c_str(), + "DNF_VERSION=%s", + version_string.c_str(), + NULL); +} + +void SystemUpgradeDownloadCommand::set_argument_parser() { + SystemUpgradeSubcommand::set_argument_parser(); + + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + auto & cmd = *get_argument_parser_command(); + + cmd.set_description("Downloads everything needed to upgrade to a new release"); + + no_downgrade = + dynamic_cast(parser.add_init_value(std::make_unique(true))); + + auto * no_downgrade_arg = parser.add_new_named_arg("no-downgrade"); + no_downgrade_arg->set_long_name("no-downgrade"); + no_downgrade_arg->set_description( + "Do not install packages from the new release if they are older than what is currently installed"); + no_downgrade_arg->link_value(no_downgrade); + + cmd.register_named_arg(no_downgrade_arg); +} + +void SystemUpgradeDownloadCommand::configure() { + SystemUpgradeSubcommand::configure(); + + auto & ctx = get_context(); + + // Check --releasever + if (get_target_releasever() == get_system_releasever()) { + throw libdnf5::cli::CommandExitError(1, M_("Need a --releasever greater than the current system version.")); + } + + ctx.set_load_system_repo(true); + ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); + + ctx.base.get_config().get_cachedir_option().set( + libdnf5::Option::Priority::PLUGINDEFAULT, get_cachedir()->get_value()); +} + +void SystemUpgradeDownloadCommand::run() { + auto & ctx = get_context(); + + const auto & goal = std::make_unique(ctx.base); + + auto & conf = ctx.base.get_config(); + conf.get_tsflags_option().set(libdnf5::Option::Priority::RUNTIME, "test"); + + if (no_downgrade->get_value()) { + goal->add_rpm_upgrade(); + } else { + goal->add_rpm_distro_sync(); + } + + auto transaction = goal->resolve(); + if (transaction.get_problems() != libdnf5::GoalProblem::NO_PROBLEM) { + throw libdnf5::cli::GoalResolveError(transaction); + } + + ctx.download_and_run(transaction); + + if (transaction.get_transaction_packages_count() == 0) { + throw libdnf5::cli::CommandExitError( + 1, M_("The system-upgrade transaction is empty; your system is already up-to-date.")); + } + + const std::string json = transaction.serialize(); + const auto & transaction_json_path = std::filesystem::path(get_datadir()) / "system-upgrade-transaction.json"; + auto transaction_json_file = libdnf5::utils::fs::File(transaction_json_path, "w"); + + transaction_json_file.write(json); + transaction_json_file.close(); + + auto state = get_state(); + state.get_data().cachedir = get_cachedir()->get_value(); + state.get_data().system_releasever = get_system_releasever(); + state.get_data().target_releasever = get_target_releasever(); + state.get_data().status = STATUS_DOWNLOAD_COMPLETE; + libdnf5::repo::RepoQuery enabled_repos(ctx.base); + enabled_repos.filter_enabled(true); + enabled_repos.filter_type(libdnf5::repo::Repo::Type::SYSTEM, libdnf5::sack::QueryCmp::NEQ); + for (const auto & repo : enabled_repos) { + state.get_data().enabled_repos.emplace_back(repo->get_id()); + } + libdnf5::repo::RepoQuery disabled_repos(ctx.base); + disabled_repos.filter_enabled(false); + disabled_repos.filter_type(libdnf5::repo::Repo::Type::SYSTEM, libdnf5::sack::QueryCmp::NEQ); + for (const auto & repo : disabled_repos) { + state.get_data().disabled_repos.emplace_back(repo->get_id()); + } + state.write(); + + std::cout << "Download complete! Use `dnf5 system-upgrade reboot` to start the upgrade." << std::endl + << "To cancel the upgrade and delete the downloaded upgrade files, use `dnf5 system-upgrade clean`." + << std::endl; +} + +void check_state(const OfflineTransactionState & state) { + const auto & read_exception = state.get_read_exception(); + if (read_exception != nullptr) { + try { + std::rethrow_exception(read_exception); + } catch (const std::exception & ex) { + const std::string message{ex.what()}; + throw libdnf5::cli::CommandExitError( + 1, M_("Error reading state: {}. Rerun `dnf5 system-upgrade download [OPTIONS]`."), message); + } + } +} + +void reboot(Context & ctx, bool poweroff = false) { + const std::string systemd_destination_name{"org.freedesktop.systemd1"}; + const std::string systemd_object_path{"/org/freedesktop/systemd1"}; + const std::string systemd_manager_interface{"org.freedesktop.systemd1.Manager"}; + + if (std::getenv("DNF_SYSTEM_UPGRADE_NO_REBOOT")) { + ctx.base.get_logger()->info("Reboot turned off, not rebooting."); + return; + } + + poweroff = !poweroff; + std::unique_ptr connection; + try { + connection = sdbus::createSystemBusConnection(); + } catch (const sdbus::Error & ex) { + const std::string error_message{ex.what()}; + throw libdnf5::cli::CommandExitError(1, M_("Couldn't connect to D-Bus: {}"), error_message); + } + auto proxy = sdbus::createProxy(systemd_destination_name, systemd_object_path); + if (poweroff) { + proxy->callMethod("Poweroff").onInterface(systemd_manager_interface); + } else { + proxy->callMethod("Reboot").onInterface(systemd_manager_interface); + } +} + +void clean(Context & ctx, const std::filesystem::path & datadir) { + ctx.base.get_logger()->info("Cleaning up downloaded data..."); + + for (const auto & entry : std::filesystem::directory_iterator(datadir)) { + std::filesystem::remove_all(entry.path()); + } +} + +void SystemUpgradeRebootCommand::set_argument_parser() { + SystemUpgradeSubcommand::set_argument_parser(); + + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + auto & cmd = *get_argument_parser_command(); + + cmd.set_description("Prepare the system to perform the upgrade and reboots to start the upgrade."); + + poweroff_after = + dynamic_cast(parser.add_init_value(std::make_unique(true))); + + auto * poweroff_after_arg = parser.add_new_named_arg("poweroff"); + poweroff_after_arg->set_long_name("poweroff"); + poweroff_after_arg->set_description("Power off the system after the operation is complete"); + poweroff_after_arg->link_value(poweroff_after); + + cmd.register_named_arg(poweroff_after_arg); +} + +void SystemUpgradeRebootCommand::run() { + auto & ctx = get_context(); + + auto state = get_state(); + check_state(state); + if (state.get_data().status != STATUS_DOWNLOAD_COMPLETE) { + throw libdnf5::cli::CommandExitError(1, M_("system is not ready for upgrade")); + } + if (std::filesystem::is_symlink(get_magic_symlink())) { + throw libdnf5::cli::CommandExitError(1, M_("upgrade is already scheduled")); + } + + if (!std::filesystem::is_directory(get_datadir())) { + throw libdnf5::cli::CommandExitError(1, M_("data directory {} does not exist"), get_datadir().string()); + } + + std::cout << "The system will now reboot to upgrade to release version " << state.get_data().target_releasever + << ". Do you want to continue?" << std::endl; + if (!libdnf5::cli::utils::userconfirm::userconfirm(ctx.base.get_config())) { + return; + } + + std::filesystem::create_symlink(get_datadir(), get_magic_symlink()); + + state.get_data().status = STATUS_READY; + state.get_data().poweroff_after = poweroff_after->get_value(); + state.write(); + + reboot(get_context(), poweroff_after->get_value()); +} + +void SystemUpgradeUpgradeCommand::set_argument_parser() {} + +void SystemUpgradeUpgradeCommand::configure() { + SystemUpgradeSubcommand::configure(); + auto & ctx = get_context(); + + ctx.set_load_system_repo(true); + ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); + + auto state = get_state(); + check_state(state); + + // Set same set of enabled/disabled repos used during `system-upgrade download` + for (const auto & repo_id : state.get_data().enabled_repos) { + ctx.setopts.emplace_back(repo_id + ".enabled", "1"); + } + for (const auto & repo_id : state.get_data().disabled_repos) { + ctx.setopts.emplace_back(repo_id + ".disabled", "1"); + } + + // Don't try to refresh metadata, we are offline + ctx.base.get_config().get_cacheonly_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, "all"); + // Don't ask any questions + ctx.base.get_config().get_assumeyes_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, true); + // Override `assumeno` too since it takes priority over `assumeyes` + ctx.base.get_config().get_assumeno_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, false); + // Upgrade operation already removes all element that must be removed. + // Additional removal could trigger unwanted changes in transaction. + ctx.base.get_config().get_clean_requirements_on_remove_option().set( + libdnf5::Option::Priority::PLUGINDEFAULT, false); + ctx.base.get_config().get_install_weak_deps_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, false); +} + +void SystemUpgradeUpgradeCommand::run() { + auto & ctx = get_context(); + + log_status("Starting offline transaction. This will take a while.", OFFLINE_STARTED_ID); + + if (!std::filesystem::is_symlink(get_magic_symlink())) { + throw libdnf5::cli::CommandExitError(0, M_("Trigger file does not exist. Exiting.")); + } + + const auto & symlinked_path = std::filesystem::read_symlink(get_magic_symlink()); + if (symlinked_path != get_datadir()) { + throw libdnf5::cli::CommandExitError(0, M_("Another upgrade tool is running. Exiting.")); + } + + std::filesystem::remove(get_magic_symlink()); + + auto state = get_state(); + if (state.get_data().status != STATUS_READY) { + throw libdnf5::cli::CommandExitError(1, M_("Use `dnf5 system-upgrade reboot` to begin the upgrade.")); + } + + const auto & transaction_json_path = std::filesystem::path(get_datadir()) / "system-upgrade-transaction.json"; + auto transaction_json_file = libdnf5::utils::fs::File(transaction_json_path, "r"); + + const auto & json = transaction_json_file.read(); + transaction_json_file.close(); + + const auto & goal = std::make_unique(ctx.base); + + auto settings = libdnf5::GoalJobSettings(); + goal->add_serialized_transaction(json, settings); + + auto transaction = goal->resolve(); + if (transaction.get_problems() != libdnf5::GoalProblem::NO_PROBLEM) { + throw libdnf5::cli::GoalResolveError(transaction); + } + + PlymouthOutput plymouth; + auto callbacks = std::make_unique(plymouth); + /* callbacks->get_multi_progress_bar()->set_total_num_of_bars(num_of_actions); */ + transaction.set_callbacks(std::move(callbacks)); + + const auto result = transaction.run(); + std::cout << std::endl; + if (result != libdnf5::base::Transaction::TransactionRunResult::SUCCESS) { + std::cerr << "Transaction failed: " << libdnf5::base::Transaction::transaction_result_to_string(result) + << std::endl; + for (auto const & entry : transaction.get_gpg_signature_problems()) { + std::cerr << entry << std::endl; + } + for (auto & problem : transaction.get_transaction_problems()) { + std::cerr << " - " << problem << std::endl; + } + throw libdnf5::cli::SilentCommandExitError(1); + } + + for (auto const & entry : transaction.get_gpg_signature_problems()) { + std::cerr << entry << std::endl; + } + + std::string upgrade_complete_message; + if (state.get_data().poweroff_after) { + upgrade_complete_message = "Upgrade complete! Cleaning up and powering off..."; + } else { + upgrade_complete_message = "Upgrade complete! Cleaning up and rebooting..."; + } + plymouth.message(upgrade_complete_message); + log_status(upgrade_complete_message, OFFLINE_STARTED_ID); + + // If the upgrade succeeded, remove downloaded data + clean(ctx, get_datadir()); + + reboot(ctx, state.get_data().poweroff_after); +} + +void SystemUpgradeCleanCommand::set_argument_parser() { + SystemUpgradeSubcommand::set_argument_parser(); +} + +void SystemUpgradeCleanCommand::run() { + auto & ctx = get_context(); + clean(ctx, get_datadir()); +} + +struct BootEntry { + std::string boot_id; + std::string timestamp; + std::string system_releasever; + std::string target_releasever; +}; + +std::string get_journal_field(sd_journal * journal, const std::string & field) { + const char * data = nullptr; + size_t length = 0; + auto rc = sd_journal_get_data(journal, field.c_str(), reinterpret_cast(&data), &length); + if (rc < 0 || data == nullptr) { + return ""; + } + const auto prefix_length = field.length() + 1; + return std::string{data + prefix_length, length - prefix_length}; +} + +std::vector find_boots(const std::string & message_id) { + std::vector boots{}; + + sd_journal * journal = nullptr; + auto rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY); + if (rc < 0) { + throw libdnf5::RuntimeError(M_("Error reading journal: {}"), std::string{std::strerror(-rc)}); + } + + const auto & uid_filter_string = fmt::format("MESSAGE_ID={}", message_id); + rc = sd_journal_add_match(journal, uid_filter_string.c_str(), 0); + if (rc < 0) { + throw libdnf5::RuntimeError(M_("Add journal match failed: {}"), std::string{std::strerror(-rc)}); + } + SD_JOURNAL_FOREACH(journal) { + uint64_t usec = 0; + rc = sd_journal_get_realtime_usec(journal, &usec); + auto sec = usec / (1000 * 1000); + boots.emplace_back(BootEntry{ + get_journal_field(journal, "_BOOT_ID"), + libdnf5::utils::string::format_epoch(sec), + get_journal_field(journal, "SYSTEM_RELEASEVER"), + get_journal_field(journal, "TARGET_RELEASEVER"), + }); + } + + return boots; +} + +void list_logs() { + const auto & boot_entries = find_boots(ID_TO_IDENTIFY_BOOTS); + + std::cout << "The following boots appear to contain upgrade logs:" << std::endl; + for (size_t index = 0; index < boot_entries.size(); index += 1) { + const auto & entry = boot_entries[index]; + std::cout << fmt::format( + "{} / {}: {} {}→{}", + index + 1, + entry.boot_id, + entry.timestamp, + entry.system_releasever, + entry.target_releasever) + << std::endl; + } + if (boot_entries.empty()) { + std::cout << "No logs were found." << std::endl; + } +} + +void show_log(size_t boot_index) { + const auto & boot_entries = find_boots(ID_TO_IDENTIFY_BOOTS); + if (boot_index >= boot_entries.size()) { + throw libdnf5::cli::CommandExitError(1, M_("Cannot find logs with this index.")); + } + + const auto & boot_id = boot_entries[boot_index].boot_id; + const auto rc = call(PATH_TO_JOURNALCTL, {"--boot", boot_id}); + + if (rc != 0 && rc != 141) { + throw libdnf5::cli::CommandExitError(1, M_("Unable to match systemd journal entry.")); + } +} + +void SystemUpgradeLogCommand::set_argument_parser() { + SystemUpgradeSubcommand::set_argument_parser(); + + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + auto & cmd = *get_argument_parser_command(); + + cmd.set_description("Show logs from past upgrades"); + + number = dynamic_cast(parser.add_init_value(std::make_unique(""))); + + auto * number_arg = parser.add_new_named_arg("number"); + number_arg->set_long_name("number"); + number_arg->set_has_value(true); + + number_arg->set_description("Which log to show. Run without any arguments to get a list of available logs."); + number_arg->link_value(number); + cmd.register_named_arg(number_arg); +} + +void SystemUpgradeLogCommand::run() { + if (number->get_value().empty()) { + list_logs(); + } else { + std::string number_string{number->get_value()}; + show_log(std::stoul(number_string) - 1); + } +} + +} // namespace dnf5 diff --git a/dnf5-plugins/system_upgrade_plugin/system_upgrade.hpp b/dnf5-plugins/system_upgrade_plugin/system_upgrade.hpp new file mode 100644 index 000000000..c976b1bc9 --- /dev/null +++ b/dnf5-plugins/system_upgrade_plugin/system_upgrade.hpp @@ -0,0 +1,166 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with libdnf. If not, see . +*/ + +#ifndef DNF5_COMMANDS_SYSTEM_UPGRADE_HPP +#define DNF5_COMMANDS_SYSTEM_UPGRADE_HPP + +#include +#include +#include +#include +#include +#include + +const std::filesystem::path PATH_TO_PLYMOUTH{"/usr/bin/plymouth"}; +const std::filesystem::path PATH_TO_JOURNALCTL{"/usr/bin/journalctl"}; +const std::string OFFLINE_STARTED_ID{"3e0a5636d16b4ca4bbe5321d06c6aa62"}; +const std::string OFFLINE_FINISHED_ID{"8cec00a1566f4d3594f116450395f06c"}; + +const std::string STATUS_DOWNLOAD_INCOMPLETE{"download-incomplete"}; +const std::string STATUS_DOWNLOAD_COMPLETE{"download-complete"}; +const std::string STATUS_READY{"ready"}; +const std::string STATUS_UPGRADE_INCOMPLETE{"upgrade-incomplete"}; + +const int STATE_VERSION = 0; +const std::string STATE_HEADER{"offline-transaction-state"}; + +struct OfflineTransactionStateData { + int state_version = STATE_VERSION; + std::string status = STATUS_DOWNLOAD_INCOMPLETE; + std::string cachedir; + std::string target_releasever; + std::string system_releasever; + bool poweroff_after = false; + std::vector enabled_repos; + std::vector disabled_repos; +}; + +TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( + OfflineTransactionStateData, + state_version, + status, + cachedir, + target_releasever, + system_releasever, + poweroff_after, + enabled_repos, + disabled_repos) + +class OfflineTransactionState { +public: + void write(); + OfflineTransactionState(std::filesystem::path path); + OfflineTransactionStateData & get_data() { return data; }; + const std::exception_ptr & get_read_exception() const { return read_exception; }; + +private: + void read(); + std::exception_ptr read_exception; + std::filesystem::path path; + OfflineTransactionStateData data; +}; + +const std::filesystem::path DEFAULT_DATADIR{std::filesystem::path(libdnf5::SYSTEM_STATE_DIR) / "system-upgrade"}; + +namespace dnf5 { + +class SystemUpgradeCommand : public Command { +public: + explicit SystemUpgradeCommand(Context & context) : Command(context, "system-upgrade") {} + void set_parent_command() override; + void set_argument_parser() override; + void register_subcommands() override; + void pre_configure() override; +}; + +class SystemUpgradeSubcommand : public Command { +public: + explicit SystemUpgradeSubcommand(Context & context, const std::string & name); + void set_argument_parser() override; + void configure() override; + +protected: + libdnf5::OptionPath * get_cachedir() const { return cachedir; }; + std::filesystem::path get_datadir() const { return datadir; }; + std::filesystem::path get_magic_symlink() const { return magic_symlink; }; + OfflineTransactionState get_state() const { return state; }; + std::string get_system_releasever() const { return system_releasever; }; + std::string get_target_releasever() const { return target_releasever; }; + void log_status(const std::string & message, const std::string & message_id) const; + +private: + libdnf5::OptionPath * cachedir{nullptr}; + std::filesystem::path datadir{DEFAULT_DATADIR}; + std::filesystem::path magic_symlink; + OfflineTransactionState state; + std::string target_releasever; + std::string system_releasever; +}; + +class SystemUpgradeDownloadCommand : public SystemUpgradeSubcommand { +public: + explicit SystemUpgradeDownloadCommand(Context & context) : SystemUpgradeSubcommand{context, "download"} {} + void set_argument_parser() override; + void configure() override; + void run() override; + +private: + libdnf5::OptionBool * no_downgrade{nullptr}; + libdnf5::OptionBool * distro_sync{nullptr}; + int state_version; +}; + +class SystemUpgradeRebootCommand : public SystemUpgradeSubcommand { +public: + explicit SystemUpgradeRebootCommand(Context & context) : SystemUpgradeSubcommand(context, "reboot") {} + void set_argument_parser() override; + void run() override; + +private: + libdnf5::OptionBool * poweroff_after{nullptr}; +}; + +class SystemUpgradeUpgradeCommand : public SystemUpgradeSubcommand { +public: + explicit SystemUpgradeUpgradeCommand(Context & context) : SystemUpgradeSubcommand(context, "upgrade") {} + void set_argument_parser() override; + void configure() override; + void run() override; +}; + +class SystemUpgradeCleanCommand : public SystemUpgradeSubcommand { +public: + explicit SystemUpgradeCleanCommand(Context & context) : SystemUpgradeSubcommand(context, "clean") {} + void set_argument_parser() override; + void run() override; +}; + +class SystemUpgradeLogCommand : public SystemUpgradeSubcommand { +public: + explicit SystemUpgradeLogCommand(Context & context) : SystemUpgradeSubcommand(context, "log") {} + void set_argument_parser() override; + void run() override; + +private: + libdnf5::OptionString * number{nullptr}; +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_CHANGELOG_HPP diff --git a/dnf5-plugins/system_upgrade_plugin/system_upgrade_cmd_plugin.cpp b/dnf5-plugins/system_upgrade_plugin/system_upgrade_cmd_plugin.cpp new file mode 100644 index 000000000..7c5970e50 --- /dev/null +++ b/dnf5-plugins/system_upgrade_plugin/system_upgrade_cmd_plugin.cpp @@ -0,0 +1,72 @@ +#include "system_upgrade.hpp" + +#include + +using namespace dnf5; + +namespace { + +constexpr const char * PLUGIN_NAME{"system_upgrade"}; +constexpr PluginVersion PLUGIN_VERSION{.major = 1, .minor = 0, .micro = 0}; + +constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr}; +constexpr const char * attrs_value[]{"Evan Goode", "egoode@redhat.com", "system_upgrade command."}; + +class SystemUpgradeCmdPlugin : public IPlugin { +public: + using IPlugin::IPlugin; + + PluginAPIVersion get_api_version() const noexcept override { return PLUGIN_API_VERSION; } + + const char * get_name() const noexcept override { return PLUGIN_NAME; } + + PluginVersion get_version() const noexcept override { return PLUGIN_VERSION; } + + const char * const * get_attributes() const noexcept override { return attrs; } + + const char * get_attribute(const char * attribute) const noexcept override { + for (size_t i = 0; attrs[i]; ++i) { + if (std::strcmp(attribute, attrs[i]) == 0) { + return attrs_value[i]; + } + } + return nullptr; + } + + std::vector> create_commands() override; + + void finish() noexcept override {} +}; + + +std::vector> SystemUpgradeCmdPlugin::create_commands() { + std::vector> commands; + commands.push_back(std::make_unique(get_context())); + return commands; +} + + +} // namespace + + +PluginAPIVersion dnf5_plugin_get_api_version(void) { + return PLUGIN_API_VERSION; +} + +const char * dnf5_plugin_get_name(void) { + return PLUGIN_NAME; +} + +PluginVersion dnf5_plugin_get_version(void) { + return PLUGIN_VERSION; +} + +IPlugin * dnf5_plugin_new_instance([[maybe_unused]] ApplicationVersion application_version, Context & context) try { + return new SystemUpgradeCmdPlugin(context); +} catch (...) { + return nullptr; +} + +void dnf5_plugin_delete_instance(IPlugin * plugin_object) { + delete plugin_object; +} diff --git a/dnf5.spec b/dnf5.spec index fd02ef56a..2b97eb810 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -174,6 +174,7 @@ BuildRequires: pkgconfig(smartcols) %if %{with dnf5_plugins} BuildRequires: libcurl-devel >= 7.62.0 +BuildRequires: systemd-devel %endif %if %{with dnf5daemon_server} @@ -653,17 +654,20 @@ Provides: dnf5-command(config-manager) Provides: dnf5-command(copr) Provides: dnf5-command(needs-restarting) Provides: dnf5-command(repoclosure) +Provides: dnf5-command(system-upgrade) %description -n dnf5-plugins Core DNF5 plugins that enhance dnf5 with builddep, changelog, config-manager, copr, and repoclosure commands. %files -n dnf5-plugins -f dnf5-plugin-builddep.lang -f dnf5-plugin-changelog.lang -f dnf5-plugin-config-manager.lang -f dnf5-plugin-copr.lang -f dnf5-plugin-needs-restarting.lang -f dnf5-plugin-repoclosure.lang +%config %{_unitdir}/dnf5-system-upgrade.service %{_libdir}/dnf5/plugins/*.so %{_mandir}/man8/dnf5-builddep.8.* %{_mandir}/man8/dnf5-copr.8.* %{_mandir}/man8/dnf5-needs-restarting.8.* %{_mandir}/man8/dnf5-repoclosure.8.* +# %{_mandir}/man8/dnf5-system-upgrade.8.* %endif diff --git a/dnf5/context.cpp b/dnf5/context.cpp index 942e69a59..43ba888f6 100644 --- a/dnf5/context.cpp +++ b/dnf5/context.cpp @@ -184,230 +184,211 @@ void Context::load_repos(bool load_system) { print_info("Repositories loaded."); } -namespace { +RpmTransCB::RpmTransCB() { + multi_progress_bar.set_total_bar_visible_limit(libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT); +} -class RpmTransCB : public libdnf5::rpm::TransactionCallbacks { -public: - RpmTransCB() { - multi_progress_bar.set_total_bar_visible_limit( - libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT); +RpmTransCB::~RpmTransCB() { + if (active_progress_bar && active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { + active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); } - - ~RpmTransCB() { - if (active_progress_bar && - active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { - active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); - } - if (active_progress_bar) { - multi_progress_bar.print(); - } - } - - libdnf5::cli::progressbar::MultiProgressBar * get_multi_progress_bar() { return &multi_progress_bar; } - - void install_progress( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - uint64_t amount, - [[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(amount)); - if (is_time_to_print()) { - multi_progress_bar.print(); - } + if (active_progress_bar) { + multi_progress_bar.print(); } +} - void install_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override { - const char * msg{nullptr}; - switch (item.get_action()) { - case libdnf5::transaction::TransactionItemAction::UPGRADE: - msg = "Upgrading "; - break; - case libdnf5::transaction::TransactionItemAction::DOWNGRADE: - msg = "Downgrading "; - break; - case libdnf5::transaction::TransactionItemAction::REINSTALL: - msg = "Reinstalling "; - break; - case libdnf5::transaction::TransactionItemAction::INSTALL: - case libdnf5::transaction::TransactionItemAction::REMOVE: - case libdnf5::transaction::TransactionItemAction::REPLACED: - break; - case libdnf5::transaction::TransactionItemAction::REASON_CHANGE: - case libdnf5::transaction::TransactionItemAction::ENABLE: - case libdnf5::transaction::TransactionItemAction::DISABLE: - case libdnf5::transaction::TransactionItemAction::RESET: - throw std::logic_error(fmt::format( - "Unexpected action in TransactionPackage: {}", - static_cast>( - item.get_action()))); - } - if (!msg) { - msg = "Installing "; - } - new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra()); - } +libdnf5::cli::progressbar::MultiProgressBar * RpmTransCB::get_multi_progress_bar() { + return &multi_progress_bar; +} - void install_stop( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - [[maybe_unused]] uint64_t amount, - [[maybe_unused]] uint64_t total) override { +void RpmTransCB::install_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, uint64_t amount, [[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(amount)); + if (is_time_to_print()) { multi_progress_bar.print(); } +} - void transaction_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(amount)); - if (is_time_to_print()) { - multi_progress_bar.print(); - } - } +void RpmTransCB::install_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) { + const char * msg{nullptr}; + switch (item.get_action()) { + case libdnf5::transaction::TransactionItemAction::UPGRADE: + msg = "Upgrading "; + break; + case libdnf5::transaction::TransactionItemAction::DOWNGRADE: + msg = "Downgrading "; + break; + case libdnf5::transaction::TransactionItemAction::REINSTALL: + msg = "Reinstalling "; + break; + case libdnf5::transaction::TransactionItemAction::INSTALL: + case libdnf5::transaction::TransactionItemAction::REMOVE: + case libdnf5::transaction::TransactionItemAction::REPLACED: + break; + case libdnf5::transaction::TransactionItemAction::REASON_CHANGE: + case libdnf5::transaction::TransactionItemAction::ENABLE: + case libdnf5::transaction::TransactionItemAction::DISABLE: + case libdnf5::transaction::TransactionItemAction::RESET: + throw std::logic_error(fmt::format( + "Unexpected action in TransactionPackage: {}", + static_cast>( + item.get_action()))); + } + if (!msg) { + msg = "Installing "; + } + new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra()); +} - void transaction_start(uint64_t total) override { - new_progress_bar(static_cast(total), "Prepare transaction"); - } +void RpmTransCB::install_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) { + multi_progress_bar.print(); +} - void transaction_stop([[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(total)); +void RpmTransCB::transaction_progress(uint64_t amount, [[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(amount)); + if (is_time_to_print()) { multi_progress_bar.print(); } +} - void uninstall_progress( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - uint64_t amount, - [[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(amount)); - if (is_time_to_print()) { - multi_progress_bar.print(); - } - } +void RpmTransCB::transaction_start(uint64_t total) { + new_progress_bar(static_cast(total), "Prepare transaction"); +} - void uninstall_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override { - const char * msg{nullptr}; - if (item.get_action() == libdnf5::transaction::TransactionItemAction::REMOVE || - item.get_action() == libdnf5::transaction::TransactionItemAction::REPLACED) { - msg = "Erasing "; - } - if (!msg) { - msg = "Cleanup "; - } - new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra()); - } +void RpmTransCB::transaction_stop([[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(total)); + multi_progress_bar.print(); +} - void uninstall_stop( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - [[maybe_unused]] uint64_t amount, - [[maybe_unused]] uint64_t total) override { +void RpmTransCB::uninstall_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, uint64_t amount, [[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(amount)); + if (is_time_to_print()) { multi_progress_bar.print(); } +} - - void unpack_error(const libdnf5::rpm::TransactionItem & item) override { - active_progress_bar->add_message( - libdnf5::cli::progressbar::MessageType::ERROR, "Unpack error: " + item.get_package().get_full_nevra()); - active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR); - multi_progress_bar.print(); +void RpmTransCB::uninstall_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) { + const char * msg{nullptr}; + if (item.get_action() == libdnf5::transaction::TransactionItemAction::REMOVE || + item.get_action() == libdnf5::transaction::TransactionItemAction::REPLACED) { + msg = "Erasing "; } - - void cpio_error(const libdnf5::rpm::TransactionItem & item) override { - active_progress_bar->add_message( - libdnf5::cli::progressbar::MessageType::ERROR, "Cpio error: " + item.get_package().get_full_nevra()); - active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR); - multi_progress_bar.print(); + if (!msg) { + msg = "Cleanup "; } + new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra()); +} - void script_error( - [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, - libdnf5::rpm::Nevra nevra, - libdnf5::rpm::TransactionCallbacks::ScriptType type, - uint64_t return_code) override { - active_progress_bar->add_message( - libdnf5::cli::progressbar::MessageType::ERROR, - fmt::format( - "Error in {} scriptlet: {} return code {}", - script_type_to_string(type), - to_full_nevra_string(nevra), - return_code)); - multi_progress_bar.print(); - } +void RpmTransCB::uninstall_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) { + multi_progress_bar.print(); +} - void script_start( - [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, - libdnf5::rpm::Nevra nevra, - libdnf5::rpm::TransactionCallbacks::ScriptType type) override { - active_progress_bar->add_message( - libdnf5::cli::progressbar::MessageType::INFO, - fmt::format("Running {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra))); - multi_progress_bar.print(); - } - void script_stop( - [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, - libdnf5::rpm::Nevra nevra, - libdnf5::rpm::TransactionCallbacks::ScriptType type, - [[maybe_unused]] uint64_t return_code) override { - active_progress_bar->add_message( - libdnf5::cli::progressbar::MessageType::INFO, - fmt::format("Stop {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra))); - multi_progress_bar.print(); - } +void RpmTransCB::unpack_error(const libdnf5::rpm::TransactionItem & item) { + active_progress_bar->add_message( + libdnf5::cli::progressbar::MessageType::ERROR, "Unpack error: " + item.get_package().get_full_nevra()); + active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR); + multi_progress_bar.print(); +} - void elem_progress( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - [[maybe_unused]] uint64_t amount, - [[maybe_unused]] uint64_t total) override { - //std::cout << "Element progress: " << header.get_full_nevra() << " " << amount << '/' << total << std::endl; - } +void RpmTransCB::cpio_error(const libdnf5::rpm::TransactionItem & item) { + active_progress_bar->add_message( + libdnf5::cli::progressbar::MessageType::ERROR, "Cpio error: " + item.get_package().get_full_nevra()); + active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR); + multi_progress_bar.print(); +} - void verify_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(amount)); - if (is_time_to_print()) { - multi_progress_bar.print(); - } - } +void RpmTransCB::script_error( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type, + uint64_t return_code) { + active_progress_bar->add_message( + libdnf5::cli::progressbar::MessageType::ERROR, + fmt::format( + "Error in {} scriptlet: {} return code {}", + script_type_to_string(type), + to_full_nevra_string(nevra), + return_code)); + multi_progress_bar.print(); +} - void verify_start([[maybe_unused]] uint64_t total) override { - new_progress_bar(static_cast(total), "Verify package files"); - } +void RpmTransCB::script_start( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type) { + active_progress_bar->add_message( + libdnf5::cli::progressbar::MessageType::INFO, + fmt::format("Running {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra))); + multi_progress_bar.print(); +} + +void RpmTransCB::script_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type, + [[maybe_unused]] uint64_t return_code) { + active_progress_bar->add_message( + libdnf5::cli::progressbar::MessageType::INFO, + fmt::format("Stop {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra))); + multi_progress_bar.print(); +} + +void RpmTransCB::elem_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) { + //std::cout << "Element progress: " << header.get_full_nevra() << " " << amount << '/' << total << std::endl; +} - void verify_stop([[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(total)); +void RpmTransCB::verify_progress(uint64_t amount, [[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(amount)); + if (is_time_to_print()) { multi_progress_bar.print(); } +} -private: - void new_progress_bar(int64_t total, const std::string & descr) { - if (active_progress_bar && - active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { - active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); - } - auto progress_bar = - std::make_unique(static_cast(total), descr); - progress_bar->set_auto_finish(false); - progress_bar->start(); - active_progress_bar = progress_bar.get(); - multi_progress_bar.add_bar(std::move(progress_bar)); - } - - static bool is_time_to_print() { - auto now = std::chrono::steady_clock::now(); - auto delta = now - prev_print_time; - auto ms = std::chrono::duration_cast(delta).count(); - if (ms > 100) { - // 100ms equals to 10 FPS and that seems to be smooth enough - prev_print_time = now; - return true; - } - return false; - } +void RpmTransCB::verify_start([[maybe_unused]] uint64_t total) { + new_progress_bar(static_cast(total), "Verify package files"); +} - static std::chrono::time_point prev_print_time; +void RpmTransCB::verify_stop([[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(total)); + multi_progress_bar.print(); +} +void RpmTransCB::new_progress_bar(int64_t total, const std::string & descr) { + if (active_progress_bar && active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { + active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + } + auto progress_bar = + std::make_unique(static_cast(total), descr); + progress_bar->set_auto_finish(false); + progress_bar->start(); + active_progress_bar = progress_bar.get(); + multi_progress_bar.add_bar(std::move(progress_bar)); +} - libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; - libdnf5::cli::progressbar::DownloadProgressBar * active_progress_bar{nullptr}; -}; +bool RpmTransCB::is_time_to_print() { + auto now = std::chrono::steady_clock::now(); + auto delta = now - prev_print_time; + auto ms = std::chrono::duration_cast(delta).count(); + if (ms > 100) { + // 100ms equals to 10 FPS and that seems to be smooth enough + prev_print_time = now; + return true; + } + return false; +} std::chrono::time_point RpmTransCB::prev_print_time = std::chrono::steady_clock::now(); -} // namespace - void Context::download_and_run(libdnf5::base::Transaction & transaction) { transaction.download(); diff --git a/dnf5/include/dnf5/context.hpp b/dnf5/include/dnf5/context.hpp index df430e4aa..f3021ba63 100644 --- a/dnf5/include/dnf5/context.hpp +++ b/dnf5/include/dnf5/context.hpp @@ -23,6 +23,7 @@ along with libdnf. If not, see . #include "version.hpp" #include +#include #include #include #include @@ -183,6 +184,85 @@ class RpmTransactionItem : public libdnf5::rpm::TransactionItem { Actions action; }; +class RpmTransCB : public libdnf5::rpm::TransactionCallbacks { +public: + RpmTransCB(); + ~RpmTransCB(); + libdnf5::cli::progressbar::MultiProgressBar * get_multi_progress_bar(); + + void install_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + uint64_t amount, + [[maybe_unused]] uint64_t total) override; + + void install_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override; + void install_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) override; + + void transaction_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override; + + void transaction_start(uint64_t total) override; + + void transaction_stop([[maybe_unused]] uint64_t total) override; + + void uninstall_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + uint64_t amount, + [[maybe_unused]] uint64_t total) override; + + void uninstall_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override; + + void uninstall_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) override; + + + void unpack_error(const libdnf5::rpm::TransactionItem & item) override; + + void cpio_error(const libdnf5::rpm::TransactionItem & item) override; + + void script_error( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type, + uint64_t return_code) override; + + void script_start( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type) override; + + void script_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type, + [[maybe_unused]] uint64_t return_code) override; + + void elem_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) override; + + void verify_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override; + + void verify_start([[maybe_unused]] uint64_t total) override; + + void verify_stop([[maybe_unused]] uint64_t total) override; + +private: + void new_progress_bar(int64_t total, const std::string & descr); + + static bool is_time_to_print(); + + static std::chrono::time_point prev_print_time; + + libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; + libdnf5::cli::progressbar::DownloadProgressBar * active_progress_bar{nullptr}; +}; + void run_transaction(libdnf5::rpm::Transaction & transaction); /// Returns the names of matching packages and paths of matching package file names and directories.