root/src/apps/softwareupdater/UpdateManager.cpp
/*
 * Copyright 2013-2019, Haiku, Inc. All Rights Reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Axel Dörfler <axeld@pinc-software.de>
 *              Rene Gollent <rene@gollent.com>
 *              Ingo Weinhold <ingo_weinhold@gmx.de>
 *              Brian Hill <supernova@tycho.email>
 *              Jacob Secunda
 */


#include "UpdateManager.h"

#include <sys/ioctl.h>
#include <unistd.h>

#include <Alert.h>
#include <Application.h>
#include <Catalog.h>
#include <Message.h>
#include <Messenger.h>
#include <NetworkInterface.h>
#include <NetworkRoster.h>
#include <Notification.h>
#include <Roster.h>

#include <package/manager/Exceptions.h>
#include <package/solver/SolverPackage.h>

#include "constants.h"
#include "ProblemWindow.h"

using namespace BPackageKit;
using namespace BPackageKit::BManager::BPrivate;

#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "UpdateManager"


UpdateManager::UpdateManager(BPackageInstallationLocation location,
        bool verbose)
        :
        BPackageManager(location, &fClientInstallationInterface, this),
        BPackageManager::UserInteractionHandler(),
        fClientInstallationInterface(),
        fStatusWindow(new SoftwareUpdaterWindow()),
        fStatusWindowMessenger(fStatusWindow),
        fProblemWindow(NULL),
        fProblemWindowMessenger(),
        fCurrentStep(ACTION_STEP_INIT),
        fChangesConfirmed(false),
        fVerbose(verbose)
{
        _SetCurrentStep(ACTION_STEP_START);
}


UpdateManager::~UpdateManager()
{
}


void
UpdateManager::CheckNetworkConnection()
{
        BNetworkRoster& roster = BNetworkRoster::Default();
        BNetworkInterface interface;
        uint32 cookie = 0;
        while (roster.GetNextInterface(&cookie, interface) == B_OK) {
                uint32 flags = interface.Flags();
                if ((flags & IFF_LOOPBACK) == 0
                        && (flags & (IFF_UP | IFF_LINK)) == (IFF_UP | IFF_LINK)) {
                        return;
                }
        }

        // No network connection detected, cannot continue
        throw BException(B_TRANSLATE_COMMENT(
                "No active network connection was found", "Error message"));
}


update_type
UpdateManager::GetUpdateType()
{
        int32 action = USER_SELECTION_NEEDED;
        if (fStatusWindowMessenger.IsValid()) {
                BMessage message(kMsgGetUpdateType);
                BMessage reply;
                fStatusWindowMessenger.SendMessage(&message, &reply);
                reply.FindInt32(kKeyAlertResult, &action);
        }
        return (update_type)action;
}


void
UpdateManager::CheckRepositories()
{
        int32 count = fOtherRepositories.CountItems();
        if (fVerbose)
                printf("Remote repositories available: %" B_PRId32 "\n", count);
        if (count == 0) {
                if (fStatusWindowMessenger.IsValid()) {
                        BMessage message(kMsgNoRepositories);
                        BMessage reply;
                        fStatusWindowMessenger.SendMessage(&message, &reply);
                        int32 result;
                        reply.FindInt32(kKeyAlertResult, &result);
                        if (result == 1)
                                be_roster->Launch("application/x-vnd.Haiku-Repositories");
                }
                be_app->PostMessage(kMsgFinalQuit);
                throw BException(B_TRANSLATE_COMMENT(
                        "No remote repositories are available", "Error message"));
        }
}


void
UpdateManager::JobFailed(BSupportKit::BJob* job)
{
        if (!fVerbose)
                return;

        BString error = job->ErrorString();
        if (error.Length() > 0) {
                error.ReplaceAll("\n", "\n*** ");
                fprintf(stderr, "%s", error.String());
        }
}


void
UpdateManager::JobAborted(BSupportKit::BJob* job)
{
        if (fVerbose)
                puts("Job aborted");
}


void
UpdateManager::FinalUpdate(const char* header, const char* text)
{
        _FinalUpdate(header, text);
}


void
UpdateManager::HandleProblems()
{
        if (fProblemWindow == NULL) {
                fProblemWindow = new ProblemWindow;
                fProblemWindowMessenger.SetTo(fProblemWindow);
        }

        ProblemWindow::SolverPackageSet installPackages;
        ProblemWindow::SolverPackageSet uninstallPackages;
        if (!fProblemWindow->Go(fSolver,installPackages, uninstallPackages))
                throw BAbortedByUserException();
        fProblemWindow->Hide();
}


void
UpdateManager::ConfirmChanges(bool fromMostSpecific)
{
        if (fVerbose)
                puts("The following changes will be made:");

        int32 count = fInstalledRepositories.CountItems();
        int32 upgradeCount = 0;
        int32 installCount = 0;
        int32 uninstallCount = 0;

        if (fromMostSpecific) {
                for (int32 i = count - 1; i >= 0; i--)
                        _PrintResult(*fInstalledRepositories.ItemAt(i), upgradeCount,
                                installCount, uninstallCount);
        } else {
                for (int32 i = 0; i < count; i++)
                        _PrintResult(*fInstalledRepositories.ItemAt(i), upgradeCount,
                                installCount, uninstallCount);
        }

        if (fVerbose)
                printf("Upgrade count=%" B_PRId32 ", Install count=%" B_PRId32
                        ", Uninstall count=%" B_PRId32 "\n",
                        upgradeCount, installCount, uninstallCount);

        fChangesConfirmed = fStatusWindow->ConfirmUpdates();
        if (!fChangesConfirmed)
                throw BAbortedByUserException();

        _SetCurrentStep(ACTION_STEP_DOWNLOAD);
        fPackageDownloadsTotal = upgradeCount + installCount;
        fPackageDownloadsCount = 1;
}


void
UpdateManager::Warn(status_t error, const char* format, ...)
{
        char buffer[256];
        va_list args;
        va_start(args, format);
        vsnprintf(buffer, sizeof(buffer), format, args);
        va_end(args);

        if (fVerbose) {
                fputs(buffer, stderr);
                if (error == B_OK)
                        puts("");
                else
                        printf(": %s\n", strerror(error));
        }

        if (fStatusWindow != NULL) {
                if (fStatusWindow->UserCancelRequested())
                        throw BAbortedByUserException();
                fStatusWindow->ShowWarningAlert(buffer);
        } else {
                BString text("SoftwareUpdater:\n");
                text.Append(buffer);
                BAlert* alert = new BAlert("warning", text, B_TRANSLATE("OK"), NULL,
                        NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
                alert->Go(NULL);
        }
}


void
UpdateManager::ProgressPackageDownloadStarted(const char* packageName)
{
        if (fCurrentStep == ACTION_STEP_DOWNLOAD) {
                BString header(B_TRANSLATE("Downloading packages"));
                _UpdateDownloadProgress(header.String(), packageName, 0.0);
                fNewDownloadStarted = false;
        }

        if (fVerbose)
                printf("Downloading %s...\n", packageName);
}


void
UpdateManager::ProgressPackageDownloadActive(const char* packageName,
        float completionValue, off_t bytes, off_t totalBytes)
{
        if (fCurrentStep == ACTION_STEP_DOWNLOAD) {
                // Fix a bug where a 100% completion percentage gets sent at the start
                // of a package download
                if (!fNewDownloadStarted) {
                        if (completionValue > 0 && completionValue < 1)
                                fNewDownloadStarted = true;
                        else
                                completionValue = 0.0;
                }
                _UpdateDownloadProgress(NULL, packageName, completionValue * 100.0);
        }

        if (fVerbose) {
                static const char* progressChars[] = {
                        "\xE2\x96\x8F",
                        "\xE2\x96\x8E",
                        "\xE2\x96\x8D",
                        "\xE2\x96\x8C",
                        "\xE2\x96\x8B",
                        "\xE2\x96\x8A",
                        "\xE2\x96\x89",
                        "\xE2\x96\x88",
                };

                int width = 70;

                struct winsize winSize;
                if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winSize) == 0
                        && winSize.ws_col < 77) {
                        // We need 7 characters for the percent display
                        width = winSize.ws_col - 7;
                }

                int position;
                int ipart = (int)(completionValue * width);
                int fpart = (int)(((completionValue * width) - ipart) * 8);

                fputs("\r", stdout); // return to the beginning of the line

                for (position = 0; position < width; position++) {
                        if (position < ipart) {
                                // This part is fully downloaded, show a full block
                                fputs(progressChars[7], stdout);
                        } else if (position > ipart) {
                                // This part is not downloaded, show a space
                                fputs(" ", stdout);
                        } else {
                                // This part is partially downloaded
                                fputs(progressChars[fpart], stdout);
                        }
                }

                // Also print the progress percentage
                printf(" %3d%%", (int)(completionValue * 100));

                fflush(stdout);
        }

}


void
UpdateManager::ProgressPackageDownloadComplete(const char* packageName)
{
        if (fCurrentStep == ACTION_STEP_DOWNLOAD) {
                _UpdateDownloadProgress(NULL, packageName, 100.0);
                fPackageDownloadsCount++;
        }

        if (fVerbose) {
                // Overwrite the progress bar with whitespace
                fputs("\r", stdout);
                struct winsize w;
                ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
                for (int i = 0; i < (w.ws_col); i++)
                        fputs(" ", stdout);
                fputs("\r\x1b[1A", stdout); // Go to previous line.

                printf("Downloading %s...done.\n", packageName);
        }
}


void
UpdateManager::ProgressPackageChecksumStarted(const char* title)
{
        // Repository checksums
        if (fCurrentStep == ACTION_STEP_START)
                _UpdateStatusWindow(NULL, title);

        if (fVerbose)
                printf("%s...", title);
}


void
UpdateManager::ProgressPackageChecksumComplete(const char* title)
{
        if (fVerbose)
                puts("done.");
}


void
UpdateManager::ProgressStartApplyingChanges(InstalledRepository& repository)
{
        _SetCurrentStep(ACTION_STEP_APPLY);
        BString header(B_TRANSLATE("Applying changes"));
        BString detail(B_TRANSLATE("Packages are being updated"));
        fStatusWindow->UpdatesApplying(header.String(), detail.String());

        if (fVerbose)
                printf("[%s] Applying changes ...\n", repository.Name().String());
}


void
UpdateManager::ProgressTransactionCommitted(InstalledRepository& repository,
        const BCommitTransactionResult& result)
{
        if (fVerbose) {
                const char* repositoryName = repository.Name().String();

                int32 issueCount = result.CountIssues();
                for (int32 i = 0; i < issueCount; i++) {
                        const BTransactionIssue* issue = result.IssueAt(i);
                        if (issue->PackageName().IsEmpty()) {
                                printf("[%s] warning: %s\n", repositoryName,
                                        issue->ToString().String());
                        } else {
                                printf("[%s] warning: package %s: %s\n", repositoryName,
                                        issue->PackageName().String(), issue->ToString().String());
                        }
                }

                printf("[%s] Changes applied. Old activation state backed up in \"%s\"\n",
                        repositoryName, result.OldStateDirectory().String());
                printf("[%s] Cleaning up ...\n", repositoryName);
        }
}


void
UpdateManager::ProgressApplyingChangesDone(InstalledRepository& repository)
{
        if (fVerbose)
                printf("[%s] Done.\n", repository.Name().String());

        _SetCurrentStep(ACTION_STEP_COMPLETE);
        BString header(B_TRANSLATE("Updates completed"));

        BString detail;
        if (BPackageRoster().IsRebootNeeded()) {
                detail = B_TRANSLATE("A reboot is necessary to complete the "
                        "update process.");
                fStatusWindow->PostMessage(kMsgShowReboot);
        } else {
                detail = B_TRANSLATE("Updates have been successfully installed.");
        }

        _FinalUpdate(header.String(), detail.String());
}


void
UpdateManager::_PrintResult(InstalledRepository& installationRepository,
        int32& upgradeCount, int32& installCount, int32& uninstallCount)
{
        if (!installationRepository.HasChanges())
                return;

        if (fVerbose)
                printf("  in %s:\n", installationRepository.Name().String());

        PackageList& packagesToActivate
                = installationRepository.PackagesToActivate();
        PackageList& packagesToDeactivate
                = installationRepository.PackagesToDeactivate();

        BStringList upgradedPackages;
        BStringList upgradedPackageVersions;
        for (int32 i = 0;
                BSolverPackage* installPackage = packagesToActivate.ItemAt(i);
                i++) {
                for (int32 j = 0;
                        BSolverPackage* uninstallPackage = packagesToDeactivate.ItemAt(j);
                        j++) {
                        if (installPackage->Info().Name() == uninstallPackage->Info().Name()) {
                                upgradedPackages.Add(installPackage->Info().Name());
                                upgradedPackageVersions.Add(uninstallPackage->Info().Version().ToString());
                                break;
                        }
                }
        }

        for (int32 i = 0; BSolverPackage* package = packagesToActivate.ItemAt(i);
                i++) {
                BString repository;
                if (dynamic_cast<MiscLocalRepository*>(package->Repository()) != NULL)
                        repository = "local file";
                else
                        repository.SetToFormat("repository %s",
                                package->Repository()->Name().String());

                int position = upgradedPackages.IndexOf(package->Info().Name());
                if (position >= 0) {
                        if (fVerbose)
                                printf("    upgrade package %s-%s to %s from %s\n",
                                        package->Info().Name().String(),
                                        upgradedPackageVersions.StringAt(position).String(),
                                        package->Info().Version().ToString().String(),
                                        repository.String());
                        fStatusWindow->AddPackageInfo(PACKAGE_UPDATE,
                                package->Info().Name().String(),
                                upgradedPackageVersions.StringAt(position).String(),
                                package->Info().Version().ToString().String(),
                                package->Info().Summary().String(),
                                package->Repository()->Name().String(),
                                package->Info().FileName().String());
                        upgradeCount++;
                } else {
                        if (fVerbose)
                                printf("    install package %s-%s from %s\n",
                                        package->Info().Name().String(),
                                        package->Info().Version().ToString().String(),
                                        repository.String());
                        fStatusWindow->AddPackageInfo(PACKAGE_INSTALL,
                                package->Info().Name().String(),
                                NULL,
                                package->Info().Version().ToString().String(),
                                package->Info().Summary().String(),
                                package->Repository()->Name().String(),
                                package->Info().FileName().String());
                        installCount++;
                }
        }

        BStringList uninstallList;
        for (int32 i = 0; BSolverPackage* package = packagesToDeactivate.ItemAt(i);
                i++) {
                if (upgradedPackages.HasString(package->Info().Name()))
                        continue;
                if (fVerbose)
                        printf("    uninstall package %s\n",
                                package->VersionedName().String());
                fStatusWindow->AddPackageInfo(PACKAGE_UNINSTALL,
                        package->Info().Name().String(),
                        package->Info().Version().ToString(),
                        NULL,
                        package->Info().Summary().String(),
                        package->Repository()->Name().String(),
                        package->Info().FileName().String());
                uninstallCount++;
        }
}


void
UpdateManager::_UpdateStatusWindow(const char* header, const char* detail)
{
        if (header == NULL && detail == NULL)
                return;

        if (fStatusWindow->UserCancelRequested())
                throw BAbortedByUserException();

        BMessage message(kMsgTextUpdate);
        if (header != NULL)
                message.AddString(kKeyHeader, header);
        if (detail != NULL)
                message.AddString(kKeyDetail, detail);
        fStatusWindow->PostMessage(&message);
}


void
UpdateManager::_UpdateDownloadProgress(const char* header,
        const char* packageName, float percentageComplete)
{
        if (packageName == NULL)
                return;

        if (fStatusWindow->UserCancelRequested())
                throw BAbortedByUserException();

        BString packageCount;
        packageCount.SetToFormat(B_TRANSLATE_COMMENT("%i of %i", "Do not translate %i"),
                (int)fPackageDownloadsCount, (int)fPackageDownloadsTotal);
        BMessage message(kMsgProgressUpdate);
        if (header != NULL)
                message.AddString(kKeyHeader, header);
        message.AddString(kKeyPackageName, packageName);
        message.AddString(kKeyPackageCount, packageCount.String());
        message.AddFloat(kKeyPercentage, percentageComplete);
        fStatusWindow->PostMessage(&message);
}


void
UpdateManager::_FinalUpdate(const char* header, const char* text)
{
        if (!fStatusWindow->IsFront()) {
                BNotification notification(B_INFORMATION_NOTIFICATION);
                notification.SetGroup("SoftwareUpdater");
                notification.SetTitle(header);
                notification.SetContent(text);
                notification.Send();
        }

        fStatusWindow->FinalUpdate(header, text);
}


void
UpdateManager::_SetCurrentStep(int32 step)
{
        fCurrentStep = step;
}