⛏️ index : haiku.git

/*
 * 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);
}