root/src/servers/package/Root.cpp
/*
 * Copyright 2013-2014, Haiku, Inc. All Rights Reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Ingo Weinhold <ingo_weinhold@gmx.de>
 */


#include "Root.h"

#include <Alert.h>
#include <Directory.h>
#include <Entry.h>
#include <package/CommitTransactionResult.h>
#include <package/PackageDefs.h>
#include <Path.h>

#include <AutoDeleter.h>
#include <AutoLocker.h>
#include <Server.h>

#include <package/DaemonDefs.h>
#include <package/manager/Exceptions.h>

#include "Constants.h"
#include "DebugSupport.h"
#include "PackageManager.h"


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


// #pragma mark - AbstractVolumeJob


struct Root::AbstractVolumeJob : public Job {
        AbstractVolumeJob(Volume* volume)
                :
                fVolume(volume)
        {
        }

        Volume* GetVolume() const
        {
                return fVolume;
        }

protected:
        Volume* fVolume;
};


// #pragma mark - VolumeJob


struct Root::VolumeJob : public AbstractVolumeJob {
        VolumeJob(Volume* volume, void (Root::*method)(Volume*))
                :
                AbstractVolumeJob(volume),
                fMethod(method)
        {
        }

        virtual void Do()
        {
                (fVolume->GetRoot()->*fMethod)(fVolume);
        }

private:
        void    (Root::*fMethod)(Volume*);
};


// #pragma mark - ProcessNodeMonitorEventsJob


struct Root::ProcessNodeMonitorEventsJob : public VolumeJob {
        ProcessNodeMonitorEventsJob(Volume* volume, void (Root::*method)(Volume*))
                :
                VolumeJob(volume, method)
        {
                fVolume->PackageJobPending();
        }

        ~ProcessNodeMonitorEventsJob()
        {
                fVolume->PackageJobFinished();
        }
};


// #pragma mark - CommitTransactionJob


struct Root::CommitTransactionJob : public AbstractVolumeJob {
        CommitTransactionJob(Root* root, Volume* volume, BMessage* message)
                :
                AbstractVolumeJob(volume),
                fRoot(root),
                fMessage(message)
        {
                fVolume->PackageJobPending();
        }

        ~CommitTransactionJob()
        {
                fVolume->PackageJobFinished();
        }

        virtual void Do()
        {
                fRoot->_CommitTransaction(fVolume, fMessage.Get());
        }

private:
        Root*                                   fRoot;
        ObjectDeleter<BMessage> fMessage;
};


// #pragma mark - VolumeJobFilter


struct Root::VolumeJobFilter : public ::JobQueue::Filter {
        VolumeJobFilter(Volume* volume)
                :
                fVolume(volume)
        {
        }

        virtual bool FilterJob(Job* job)
        {
                AbstractVolumeJob* volumeJob = dynamic_cast<AbstractVolumeJob*>(job);
                return volumeJob != NULL && volumeJob->GetVolume() == fVolume;
        }

private:
        Volume* fVolume;
};


// #pragma mark - Root


Root::Root()
        :
        fLock("packagefs root"),
        fNodeRef(),
        fIsSystemRoot(false),
        fPath(),
        fSystemVolume(NULL),
        fHomeVolume(NULL),
        fJobQueue(),
        fJobRunner(-1)
{
}


Root::~Root()
{
        fJobQueue.Close();

        if (fJobRunner >= 0)
                wait_for_thread(fJobRunner, NULL);
}


status_t
Root::Init(const node_ref& nodeRef, bool isSystemRoot)
{
        fNodeRef = nodeRef;
        fIsSystemRoot = isSystemRoot;

        // init members and spawn job runner thread
        status_t error = fJobQueue.Init();
        if (error != B_OK)
                RETURN_ERROR(error);

        error = fLock.InitCheck();
        if (error != B_OK)
                RETURN_ERROR(error);

        fJobRunner = spawn_thread(&_JobRunnerEntry, "job runner", B_NORMAL_PRIORITY,
                this);
        if (fJobRunner < 0)
                RETURN_ERROR(fJobRunner);

        // get the path
        BDirectory directory;
        error = directory.SetTo(&fNodeRef);
        if (error != B_OK) {
                ERROR("Root::Init(): failed to open directory: %s\n", strerror(error));
                RETURN_ERROR(error);
        }

        BEntry entry;
        error = directory.GetEntry(&entry);

        BPath path;
        if (error == B_OK)
                error = entry.GetPath(&path);

        if (error != B_OK) {
                ERROR("Root::Init(): failed to get directory path: %s\n",
                        strerror(error));
                RETURN_ERROR(error);
        }

        fPath = path.Path();
        if (fPath.IsEmpty())
                RETURN_ERROR(B_NO_MEMORY);

        resume_thread(fJobRunner);

        return B_OK;
}


status_t
Root::RegisterVolume(Volume* volume)
{
        AutoLocker<BLocker> locker(fLock);

        Volume** volumeToSet = _GetVolume(volume->MountType());
        if (volumeToSet == NULL)
                return B_BAD_VALUE;

        if (*volumeToSet != NULL) {
                ERROR("Root::RegisterVolume(): can't register volume at \"%s\", since "
                        "there's already volume at \"%s\" with the same type.\n",
                        volume->Path().String(), (*volumeToSet)->Path().String());
                return B_BAD_VALUE;
        }

        *volumeToSet = volume;
        volume->SetRoot(this);

        // queue a job for reading the volume's packages
        status_t error = _QueueJob(
                new(std::nothrow) VolumeJob(volume, &Root::_InitPackages));
        if (error != B_OK) {
                volume->SetRoot(NULL);
                *volumeToSet = NULL;
                return error;
        }

        return B_OK;
}


void
Root::UnregisterVolume(Volume* volume)
{
        AutoLocker<BLocker> locker(fLock);

        Volume** volumeToSet = _GetVolume(volume->MountType());
        if (volumeToSet == NULL || *volumeToSet != volume) {
                ERROR("Root::UnregisterVolume(): can't unregister unknown volume at "
                        "\"%s.\n", volume->Path().String());
                return;
        }

        *volumeToSet = NULL;

        // Use the job queue to delete the volume to make sure there aren't any
        // pending jobs that reference the volume.
        _QueueJob(new(std::nothrow) VolumeJob(volume, &Root::_DeleteVolume));
}


Volume*
Root::FindVolume(dev_t deviceID) const
{
        AutoLocker<BLocker> locker(fLock);

        Volume* volumes[] = { fSystemVolume, fHomeVolume };
        for (size_t i = 0; i < sizeof(volumes) / sizeof(volumes[0]); i++) {
                Volume* volume = volumes[i];
                if (volume != NULL && volume->DeviceID() == deviceID)
                        return volume;
        }

        return NULL;
}


Volume*
Root::GetVolume(BPackageInstallationLocation location)
{
        switch ((BPackageInstallationLocation)location) {
                case B_PACKAGE_INSTALLATION_LOCATION_SYSTEM:
                        return fSystemVolume;
                case B_PACKAGE_INSTALLATION_LOCATION_HOME:
                        return fHomeVolume;
                default:
                        return NULL;
        }
}


void
Root::HandleRequest(BMessage* message)
{
        ObjectDeleter<BMessage> messageDeleter(message);

        // get the location and the volume
        int32 location;
        if (message->FindInt32("location", &location) != B_OK
                || location < 0
                || location >= B_PACKAGE_INSTALLATION_LOCATION_ENUM_COUNT) {
                return;
        }

        AutoLocker<BLocker> locker(fLock);

        Volume* volume = GetVolume((BPackageInstallationLocation)location);
        if (volume == NULL)
                return;

        switch (message->what) {
                case B_MESSAGE_GET_INSTALLATION_LOCATION_INFO:
                        volume->HandleGetLocationInfoRequest(message);
                        break;

                case B_MESSAGE_COMMIT_TRANSACTION:
                {
                        // The B_MESSAGE_COMMIT_TRANSACTION request must be handled in the
                        // job thread. But only queue a job, if there aren't package jobs
                        // pending already.
                        if (volume->IsPackageJobPending()) {
                                BMessage reply(B_MESSAGE_COMMIT_TRANSACTION_REPLY);
                                BCommitTransactionResult result(
                                        B_TRANSACTION_INSTALLATION_LOCATION_BUSY);
                                if (result.AddToMessage(reply) == B_OK) {
                                        message->SendReply(&reply, (BHandler*)NULL,
                                                kCommunicationTimeout);
                                }
                                return;
                        }

                        CommitTransactionJob* job = new(std::nothrow) CommitTransactionJob(
                                this, volume, message);
                        if (job == NULL)
                                return;

                        messageDeleter.Detach();

                        _QueueJob(job);
                        break;
                }

                default:
                        break;
        }
}


void
Root::VolumeNodeMonitorEventOccurred(Volume* volume)
{
        _QueueJob(new(std::nothrow) ProcessNodeMonitorEventsJob(volume,
                &Root::_ProcessNodeMonitorEvents));
}


void
Root::LastReferenceReleased()
{
}


Volume**
Root::_GetVolume(PackageFSMountType mountType)
{
        switch (mountType) {
                case PACKAGE_FS_MOUNT_TYPE_SYSTEM:
                        return &fSystemVolume;
                case PACKAGE_FS_MOUNT_TYPE_HOME:
                        return &fHomeVolume;
                case PACKAGE_FS_MOUNT_TYPE_CUSTOM:
                default:
                        return NULL;
        }
}


Volume*
Root::_NextVolumeFor(Volume* volume)
{
        if (volume == NULL)
                return NULL;

        PackageFSMountType mountType = volume->MountType();

        do {
                switch (mountType) {
                        case PACKAGE_FS_MOUNT_TYPE_HOME:
                                mountType = PACKAGE_FS_MOUNT_TYPE_SYSTEM;
                                break;
                        case PACKAGE_FS_MOUNT_TYPE_SYSTEM:
                        case PACKAGE_FS_MOUNT_TYPE_CUSTOM:
                        default:
                                return NULL;
                }

                volume = *_GetVolume(mountType);
        } while (volume == NULL);

        return volume;
}


void
Root::_InitPackages(Volume* volume)
{
        if (volume->InitPackages(this) == B_OK) {
                AutoLocker<BLocker> locker(fLock);
                Volume* nextVolume = _NextVolumeFor(volume);
                Volume* nextNextVolume = _NextVolumeFor(nextVolume);
                locker.Unlock();

                volume->InitialVerify(nextVolume, nextNextVolume);
        }
}


void
Root::_DeleteVolume(Volume* volume)
{
        // delete all pending jobs for that volume
        VolumeJobFilter filter(volume);
        fJobQueue.DeleteJobs(&filter);

        delete volume;
}


void
Root::_ProcessNodeMonitorEvents(Volume* volume)
{
        volume->ProcessPendingNodeMonitorEvents();

        if (!volume->HasPendingPackageActivationChanges())
                return;

        // If this is not the system root, just activate/deactivate the packages.
        if (!fIsSystemRoot) {
                volume->ProcessPendingPackageActivationChanges();
                return;
        }

        // For the system root do the full dependency analysis.

        PRINT("Root::_ProcessNodeMonitorEvents(): running package manager...\n");
        try {
                PackageManager packageManager(this, volume);
                packageManager.HandleUserChanges();
        } catch (BNothingToDoException&) {
                PRINT("Root::_ProcessNodeMonitorEvents(): -> nothing to do\n");
        } catch (std::bad_alloc&) {
                _ShowError(
                        "Insufficient memory while trying to apply package changes.");
        } catch (BFatalErrorException& exception) {
                if (exception.Error() == B_OK) {
                        _ShowError(exception.Message());
                } else {
                        _ShowError(BString().SetToFormat("%s: %s",
                                exception.Message().String(), strerror(exception.Error())));
                }
                // TODO: Print exception.Details()?
        } catch (BAbortedByUserException&) {
                PRINT("Root::_ProcessNodeMonitorEvents(): -> aborted by user\n");
        }

        volume->ClearPackageActivationChanges();
}


void
Root::_CommitTransaction(Volume* volume, BMessage* message)
{
        volume->HandleCommitTransactionRequest(message);
}


status_t
Root::_QueueJob(Job* job)
{
        if (job == NULL)
                return B_NO_MEMORY;

        BReference<Job> jobReference(job, true);
        if (!fJobQueue.QueueJob(job)) {
                // job queue already closed
                return B_BAD_VALUE;
        }

        return B_OK;
}


/*static*/ status_t
Root::_JobRunnerEntry(void* data)
{
        return ((Root*)data)->_JobRunner();
}


status_t
Root::_JobRunner()
{
        while (Job* job = fJobQueue.DequeueJob()) {
                job->Do();
                job->ReleaseReference();
        }

        return B_OK;
}


/*static*/ void
Root::_ShowError(const char* errorMessage)
{
        BServer* server = dynamic_cast<BServer*>(be_app);
        if (server != NULL && server->InitGUIContext() == B_OK) {
                BAlert* alert = new(std::nothrow) BAlert("Package error",
                        errorMessage, "OK", NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
                if (alert != NULL) {
                        alert->SetShortcut(0, B_ESCAPE);
                        alert->Go();
                        return;
                }
        }

        ERROR("Root::_ShowError(): %s\n", errorMessage);
}