root/src/kits/storage/AddOnMonitorHandler.cpp
/*
 * Copyright 2004-2013 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Stephan Aßmus, superstippi@gmx.de
 *              Andrew Bachmann
 *              John Scipione, jscipione@gmail.com
 */


#include "AddOnMonitorHandler.h"

#include <string.h>
#include <strings.h>

#include <Autolock.h>
#include <Directory.h>
#include <FindDirectory.h>
#include <Path.h>

#include <driver_settings.h>
#include <safemode_defs.h>
#include <syscalls.h>


#ifndef ADD_ON_STABLE_SECONDS
#       define ADD_ON_STABLE_SECONDS 1
#endif


AddOnMonitorHandler::AddOnMonitorHandler(const char* name)
        :
        NodeMonitorHandler(name != NULL ? name : "AddOnMonitorHandler")
{
}


AddOnMonitorHandler::~AddOnMonitorHandler()
{
        // TODO: Actually calling watch_node() here should be too late, since we
        // are likely not attached to a looper anymore, and thus consitute no valid
        // BMessenger at this time.
        DirectoryList::iterator it = fDirectories.begin();
        for (; it != fDirectories.end(); it++) {
                EntryList::iterator eiter = it->entries.begin();
                for (; eiter != it->entries.end(); eiter++)
                        watch_node(&eiter->addon_nref, B_STOP_WATCHING, this);
                watch_node(&it->nref, B_STOP_WATCHING, this);
        }
}


void
AddOnMonitorHandler::MessageReceived(BMessage* msg)
{
        if (msg->what == B_PULSE)
                _HandlePendingEntries();

        inherited::MessageReceived(msg);
}


status_t
AddOnMonitorHandler::AddDirectory(const node_ref* nref, bool sync)
{
        // Keep the looper thread locked, since this method is likely to be called
        // in a thread other than the looper thread. Otherwise we may access the
        // lists concurrently with the looper thread, when node monitor
        // notifications arrive while we are still adding initial entries from the
        // directory, or (much more likely) if the looper thread handles a pulse
        // message and wants to process pending entries while we are still adding
        // them.
        BAutolock _(Looper());

        // ignore directories added twice
        DirectoryList::iterator it = fDirectories.begin();
        for (; it != fDirectories.end(); it++) {
                if (it->nref == *nref)
                        return B_OK;
        }

        BDirectory directory(nref);
        status_t status = directory.InitCheck();
        if (status != B_OK)
                return status;

        status = watch_node(nref, B_WATCH_DIRECTORY, this);
        if (status != B_OK)
                return status;

        add_on_directory_info dirInfo;
        dirInfo.nref = *nref;
        fDirectories.push_back(dirInfo);

        add_on_entry_info entryInfo;
        entryInfo.dir_nref = *nref;

        BEntry entry;
        while (directory.GetNextEntry(&entry) == B_OK) {
                if (entry.GetName(entryInfo.name) != B_OK
                        || entry.GetNodeRef(&entryInfo.nref) != B_OK) {
                        continue;
                }

                fPendingEntries.push_back(entryInfo);
        }

        if (sync)
                _HandlePendingEntries();

        return B_OK;
}


status_t
AddOnMonitorHandler::AddAddOnDirectories(const char* leafPath)
{
        char parameter[32];
        size_t parameterLength = sizeof(parameter);
        uint32 start = 0;

        const directory_which addOnDirectories[] = {
                B_USER_NONPACKAGED_ADDONS_DIRECTORY,
                B_USER_ADDONS_DIRECTORY,
                B_SYSTEM_NONPACKAGED_ADDONS_DIRECTORY,
                B_SYSTEM_ADDONS_DIRECTORY
        };

        if (_kern_get_safemode_option(B_SAFEMODE_DISABLE_USER_ADD_ONS, parameter,
                        &parameterLength) == B_OK) {
                if (!strcasecmp(parameter, "enabled") || !strcasecmp(parameter, "on")
                        || !strcasecmp(parameter, "true") || !strcasecmp(parameter, "yes")
                        || !strcasecmp(parameter, "enable") || !strcmp(parameter, "1")) {
                        // skip user add on directories
                        start = 2;
                }
        }

        if (_kern_get_safemode_option(B_SAFEMODE_SAFE_MODE, parameter,
                        &parameterLength) == B_OK) {
                if (!strcasecmp(parameter, "enabled") || !strcasecmp(parameter, "on")
                        || !strcasecmp(parameter, "true") || !strcasecmp(parameter, "yes")
                        || !strcasecmp(parameter, "enable") || !strcmp(parameter, "1")) {
                        // safe mode, only B_SYSTEM_ADDONS_DIRECTORY is used
                        start = 3;
                }
        }

        for (uint32 i = start;
                        i < sizeof(addOnDirectories) / sizeof(directory_which); i++) {
                BDirectory directory;
                node_ref nodeRef;
                BPath path;
                if (find_directory(addOnDirectories[i], &path) == B_OK
                        && path.Append(leafPath) == B_OK
                        && directory.SetTo(path.Path()) == B_OK
                        && directory.GetNodeRef(&nodeRef) == B_OK) {
                        status_t result = this->AddDirectory(&nodeRef);
                        if (result != B_OK)
                                return result;
                }
        }

        return B_OK;
}


//      #pragma mark - AddOnMonitorHandler hooks


void
AddOnMonitorHandler::AddOnCreated(const add_on_entry_info* entryInfo)
{

}


void
AddOnMonitorHandler::AddOnEnabled(const add_on_entry_info* entryInfo)
{
}


void
AddOnMonitorHandler::AddOnDisabled(const add_on_entry_info* entryInfo)
{
}


void
AddOnMonitorHandler::AddOnRemoved(const add_on_entry_info* entryInfo)
{
}


//      #pragma mark - NodeMonitorHandler hooks


void
AddOnMonitorHandler::EntryCreated(const char* name, ino_t directory,
        dev_t device, ino_t node)
{
        add_on_entry_info entryInfo;
        strlcpy(entryInfo.name, name, sizeof(entryInfo.name));
        make_node_ref(device, node, &entryInfo.nref);
        make_node_ref(device, directory, &entryInfo.dir_nref);
        fPendingEntries.push_back(entryInfo);
}


void
AddOnMonitorHandler::EntryRemoved(const char* name, ino_t directory,
        dev_t device, ino_t node)
{
        node_ref entryNodeRef;
        make_node_ref(device, node, &entryNodeRef);

        // Search pending entries first, which can simply be discarded
        // We might have this entry in the pending list multiple times,
        // so we search entire list through, even after finding one.
        EntryList::iterator eiter = fPendingEntries.begin();
        while (eiter != fPendingEntries.end()) {
                if (eiter->nref == entryNodeRef)
                        eiter = fPendingEntries.erase(eiter);
                else
                        eiter++;
        }

        // Find the directory of the entry.
        DirectoryList::iterator diter = fDirectories.begin();
        if (!_FindDirectory(directory, device, diter)) {
                // If it was not found, we're done
                return;
        }

        eiter = diter->entries.begin();
        if (!_FindEntry(entryNodeRef, diter->entries, eiter)) {
                // This must be the directory, but we didn't find the entry.
                return;
        }

        add_on_entry_info info = *eiter;
        watch_node(&entryNodeRef, B_STOP_WATCHING, this);
        diter->entries.erase(eiter);

        // Start at the top again, and search until the directory we found
        // the old add-on in. If we find an add-on with the same name then
        // the old add-on was not enabled. So we deallocate the old add-on and
        // return.
        DirectoryList::iterator diter2 = fDirectories.begin();
        for (; diter2 != diter; diter2++) {
                if (_HasEntry(info.name, diter2->entries)) {
                        AddOnRemoved(&info);
                        return;
                }
        }

        // An active add-on was removed. We need to disable and then subsequently
        // deallocate it.
        AddOnDisabled(&info);
        AddOnRemoved(&info);

        // Continue searching for an add-on below us. If we find an add-on
        // with the same name, we must enable it.
        for (diter++; diter != fDirectories.end(); diter++) {
                eiter = diter->entries.begin();
                if (_FindEntry(info.name, diter->entries, eiter)) {
                        AddOnEnabled(&*eiter);
                        break;
                }
        }
}


void
AddOnMonitorHandler::EntryMoved(const char* name, const char* fromName,
        ino_t fromDirectory, ino_t toDirectory, dev_t device, ino_t node,
        dev_t nodeDevice)
{
        node_ref toNodeRef;
        make_node_ref(device, toDirectory, &toNodeRef);

        // Search the "from" and "to" directory in the known directories
        DirectoryList::iterator fromIter = fDirectories.begin();
        bool watchingFromDirectory = _FindDirectory(fromDirectory, device,
                fromIter);

        DirectoryList::iterator toIter = fDirectories.begin();
        bool watchingToDirectory = _FindDirectory(toNodeRef, toIter);

        if (!watchingFromDirectory && !watchingToDirectory) {
                // It seems the notification was for a directory we are not
                // actually watching (at least not by intention).
                return;
        }

        add_on_entry_info info;

        node_ref entryNodeRef;
        make_node_ref(device, node, &entryNodeRef);

        if (!watchingToDirectory) {
                // moved out of our view
                EntryList::iterator eiter = fromIter->entries.begin();
                if (!_FindEntry(entryNodeRef, fromIter->entries, eiter)) {
                        // we don't know anything about this entry yet.. ignore it
                        return;
                }

                // save the info and remove the entry
                info = *eiter;
                watch_node(&entryNodeRef, B_STOP_WATCHING, this);
                fromIter->entries.erase(eiter);

                // Start at the top again, and search until the from directory.
                // If we find a add-on with the same name then the moved add-on
                // was not enabled.  So we are done.
                DirectoryList::iterator diter = fDirectories.begin();
                for (; diter != fromIter; diter++) {
                        eiter = diter->entries.begin();
                        if (_FindEntry(info.name, diter->entries, eiter))
                                return;
                }

                // finally disable the add-on
                AddOnDisabled(&info);

                // Continue searching for a add-on below us.  If we find a add-on
                // with the same name, we must enable it.
                for (fromIter++; fromIter != fDirectories.end(); fromIter++) {
                        eiter = fromIter->entries.begin();
                        if (_FindEntry(info.name, fromIter->entries, eiter)) {
                                AddOnEnabled(&*eiter);
                                return;
                        }
                }

                // finally destroy the addon
                AddOnRemoved(&info);

                // done
                return;
        }

        if (!watchingFromDirectory) {
                // moved into our view

                // update the info
                strlcpy(info.name, name, sizeof(info.name));
                info.nref = entryNodeRef;
                info.dir_nref = toNodeRef;

                AddOnCreated(&info);

                // Start at the top again, and search until the to directory.
                // If we find an add-on with the same name then the moved add-on
                // is not to be enabled. So we are done.
                DirectoryList::iterator diter = fDirectories.begin();
                for (; diter != toIter; diter++) {
                        if (_HasEntry(info.name, diter->entries)) {
                                // The new add-on is being shadowed.
                                return;
                        }
                }

                // The new add-on should be enabled, but first we check to see
                // if there is an add-on below us. If we find one, we disable it.
                for (diter++ ; diter != fDirectories.end(); diter++) {
                        EntryList::iterator eiter = diter->entries.begin();
                        if (_FindEntry(info.name, diter->entries, eiter)) {
                                AddOnDisabled(&*eiter);
                                break;
                        }
                }

                // enable the new add-on
                AddOnEnabled(&info);

                // put the new entry into the target directory
                _AddNewEntry(toIter->entries, info);

                // done
                return;
        }

        // The add-on was renamed, or moved within our hierarchy.

        EntryList::iterator eiter = fromIter->entries.begin();
        if (_FindEntry(entryNodeRef, fromIter->entries, eiter)) {
                // save the old info and remove the entry
                info = *eiter;
        } else {
                // If an entry moved from one watched directory into another watched
                // directory, there will be two notifications, and this may be the
                // second. We have handled everything in the first. In that case the
                // entry was already removed from the fromDirectory and added in the
                // toDirectory list.
                return;
        }

        if (strcmp(info.name, name) == 0) {
                // It should be impossible for the name to stay the same, unless the
                // node moved in the watched hierarchy. Handle this case by removing
                // the entry and readding it. TODO: This can temporarily enable add-ons
                // which should in fact stay hidden (moving add-on from home to common
                // folder or vice versa, the system add-on should remain hidden).
                EntryRemoved(name, fromDirectory, device, node);
                info.dir_nref = toNodeRef;
                _EntryCreated(info);
        } else {
                // Erase the entry
                fromIter->entries.erase(eiter);

                // check to see if it was formerly enabled
                bool wasEnabled = true;
                DirectoryList::iterator oldIter = fDirectories.begin();
                for (; oldIter != fromIter; oldIter++) {
                        if (_HasEntry(info.name, oldIter->entries)) {
                                wasEnabled = false;
                                break;
                        }
                }

                // If it was enabled, disable it and enable the one under us, if it
                // exists.
                if (wasEnabled) {
                        AddOnDisabled(&info);
                        for (; oldIter != fDirectories.end(); oldIter++) {
                                eiter = oldIter->entries.begin();
                                if (_FindEntry(info.name, oldIter->entries, eiter)) {
                                        AddOnEnabled(&*eiter);
                                        break;
                                }
                        }
                }

                // kaboom!
                AddOnRemoved(&info);

                // set up new addon info
                strlcpy(info.name, name, sizeof(info.name));
                info.dir_nref = toNodeRef;

                // presto!
                AddOnCreated(&info);

                // check to see if we are newly enabled
                bool isEnabled = true;
                DirectoryList::iterator newIter = fDirectories.begin();
                for (; newIter != toIter; newIter++) {
                        if (_HasEntry(info.name, newIter->entries)) {
                                isEnabled = false;
                                break;
                        }
                }

                // if it is newly enabled, check under us for an enabled one, and
                // disable that first
                if (isEnabled) {
                        for (; newIter != fDirectories.end(); newIter++) {
                                eiter = newIter->entries.begin();
                                if (_FindEntry(info.name, newIter->entries, eiter)) {
                                        AddOnDisabled(&*eiter);
                                        break;
                                }
                        }
                        AddOnEnabled(&info);
                }
                // put the new entry into the target directory
                toIter->entries.push_back(info);
        }
}


void
AddOnMonitorHandler::StatChanged(ino_t node, dev_t device, int32 statFields)
{
        // This notification is received for the add-ons themselves.

        // TODO: Add the entry to the pending list, disable/enable it
        // when the modification time remains stable.

        node_ref entryNodeRef;
        make_node_ref(device, node, &entryNodeRef);

        DirectoryList::iterator diter = fDirectories.begin();
        for (; diter != fDirectories.end(); diter++) {
                EntryList::iterator eiter = diter->entries.begin();
                for (; eiter != diter->entries.end(); eiter++) {
                        if (eiter->addon_nref == entryNodeRef) {
                                // Trigger reloading of the add-on
                                const add_on_entry_info* info = &*eiter;
                                AddOnDisabled(info);
                                AddOnRemoved(info);
                                AddOnCreated(info);
                                AddOnEnabled(info);
                                return;
                        }
                }
        }
}


// #pragma mark - private


//!     Process pending entries.
void
AddOnMonitorHandler::_HandlePendingEntries()
{
        BDirectory directory;
        EntryList::iterator iter = fPendingEntries.begin();
        while (iter != fPendingEntries.end()) {
                add_on_entry_info info = *iter;

                // Initialize directory, or re-use from previous iteration, if
                // directory node_ref remained the same from the last pending entry.
                node_ref dirNodeRef;
                if (directory.GetNodeRef(&dirNodeRef) != B_OK
                        || dirNodeRef != info.dir_nref) {
                        if (directory.SetTo(&info.dir_nref) != B_OK) {
                                // invalid directory, discard this pending entry
                                iter = fPendingEntries.erase(iter);
                                continue;
                        }
                        dirNodeRef = info.dir_nref;
                }

                struct stat st;
                if (directory.GetStatFor(info.name, &st) != B_OK) {
                        // invalid file name, discard this pending entry
                        iter = fPendingEntries.erase(iter);
                        continue;
                }

                // stat units are seconds, real_time_clock units are seconds
                if (real_time_clock() - st.st_mtime < ADD_ON_STABLE_SECONDS) {
                        // entry not stable, skip the entry for this pulse
                        iter++;
                        continue;
                }

                // we are going to deal with the stable entry, so remove it
                iter = fPendingEntries.erase(iter);

                _EntryCreated(info);
        }
}


void
AddOnMonitorHandler::_EntryCreated(add_on_entry_info& info)
{
        // put the new entry into the directory info
        DirectoryList::iterator diter = fDirectories.begin();
        for (; diter != fDirectories.end(); diter++) {
                if (diter->nref == info.dir_nref) {
                        _AddNewEntry(diter->entries, info);
                        break;
                }
        }

        // report it
        AddOnCreated(&info);

        // Start at the top again, and search until the directory we put
        // the new add-on in.  If we find an add-on with the same name then
        // the new add-on should not be enabled.
        bool enabled = true;
        DirectoryList::iterator diter2 = fDirectories.begin();
        for (; diter2 != diter; diter2++) {
                if (_HasEntry(info.name, diter2->entries)) {
                        enabled = false;
                        break;
                }
        }
        if (!enabled)
                return;

        // The new add-on should be enabled, but first we check to see
        // if there is an add-on shadowed by the new one.  If we find one,
        // we disable it.
        for (diter++ ; diter != fDirectories.end(); diter++) {
                EntryList::iterator eiter = diter->entries.begin();
                if (_FindEntry(info.name, diter->entries, eiter)) {
                        AddOnDisabled(&*eiter);
                        break;
                }
        }

        // enable the new entry
        AddOnEnabled(&info);
}


bool
AddOnMonitorHandler::_FindEntry(const node_ref& entry, const EntryList& list,
        EntryList::iterator& it) const
{
        for (; EntryList::const_iterator(it) != list.end(); it++) {
                if (it->nref == entry)
                        return true;
        }
        return false;
}


bool
AddOnMonitorHandler::_FindEntry(const char* name, const EntryList& list,
        EntryList::iterator& it) const
{
        for (; EntryList::const_iterator(it) != list.end(); it++) {
                if (strcmp(it->name, name) == 0)
                        return true;
        }
        return false;
}


bool
AddOnMonitorHandler::_HasEntry(const node_ref& entry, EntryList& list) const
{
        EntryList::iterator it = list.begin();
        return _FindEntry(entry, list, it);
}


bool
AddOnMonitorHandler::_HasEntry(const char* name, EntryList& list) const
{
        EntryList::iterator it = list.begin();
        return _FindEntry(name, list, it);
}


bool
AddOnMonitorHandler::_FindDirectory(ino_t directory, dev_t device,
        DirectoryList::iterator& it) const
{
        node_ref nodeRef;
        make_node_ref(device, directory, &nodeRef);
        return _FindDirectory(nodeRef, it, fDirectories.end());
}


bool
AddOnMonitorHandler::_FindDirectory(const node_ref& directoryNodeRef,
        DirectoryList::iterator& it) const
{
        return _FindDirectory(directoryNodeRef, it, fDirectories.end());
}


bool
AddOnMonitorHandler::_FindDirectory(ino_t directory, dev_t device,
        DirectoryList::iterator& it,
        const DirectoryList::const_iterator& end) const
{
        node_ref nodeRef;
        make_node_ref(device, directory, &nodeRef);
        return _FindDirectory(nodeRef, it, end);
}


bool
AddOnMonitorHandler::_FindDirectory(const node_ref& directoryNodeRef,
        DirectoryList::iterator& it,
        const DirectoryList::const_iterator& end) const
{
        for (; DirectoryList::const_iterator(it) != end; it++) {
                if (it->nref == directoryNodeRef)
                        return true;
        }
        return false;
}


void
AddOnMonitorHandler::_AddNewEntry(EntryList& list, add_on_entry_info& info)
{
        BDirectory directory(&info.dir_nref);
        BEntry entry(&directory, info.name, true);

        node_ref addOnRef;      
        if (entry.GetNodeRef(&addOnRef) == B_OK) {
                watch_node(&addOnRef, B_WATCH_STAT, this);
                info.addon_nref = addOnRef;
        } else
                info.addon_nref = info.nref;

        list.push_back(info);
}