OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-sensory / opencog / atoms / filedir / FileWatcher.cc

287 lines · cc ref:
/*
 * opencog/atoms/filedir/FileWatcher.cc
 *
 * Copyright (C) 2025 BrainyBlaze Dynamics, Inc.
 * All Rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License v3 as
 * published by the Free Software Foundation and including the exceptions
 * at http://opencog.org/wiki/Licenses
 *
 * This program 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 Affero General Public License
 * along with this program; if not, write to:
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <errno.h>
#include <limits.h>
#include <poll.h>
#include <string.h>
#include <sys/inotify.h>
#include <unistd.h>

#include <opencog/util/exceptions.h>
#include <opencog/atoms/value/StringValue.h>
#include <opencog/atoms/value/ValueFactory.h>

#include "FileWatcher.h"

using namespace opencog;

FileWatcher::FileWatcher() :
	_inotify_fd(-1),
	_watch_fd(-1),
	_watch_path(),
	_event_mask(0)
{
}

FileWatcher::~FileWatcher()
{
	stop_watching();
	cleanup_watch();
	cleanup_inotify();
}

void FileWatcher::cleanup_watch()
{
	if (_watch_fd >= 0 && _inotify_fd >= 0)
	{
		inotify_rm_watch(_inotify_fd, _watch_fd);
		_watch_fd = -1;
	}
	_watch_path.clear();
	_event_mask = 0;
}

void FileWatcher::cleanup_inotify()
{
	if (_inotify_fd >= 0)
	{
		::close(_inotify_fd);
		_inotify_fd = -1;
	}
}

void FileWatcher::add_watch(const std::string& path)
{
	std::lock_guard<std::mutex> lock(_mtx);

	// Remove any existing watch first
	if (_watch_fd >= 0)
		cleanup_watch();

	// Initialize inotify if not already done
	if (_inotify_fd < 0)
	{
		_inotify_fd = inotify_init1(IN_NONBLOCK);
		if (_inotify_fd < 0)
		{
			int norr = errno;
			throw RuntimeException(TRACE_INFO,
				"Failed to initialize inotify: %s\n", strerror(norr));
		}
	}

	// Event mask for watching files and directories
	uint32_t mask = IN_CREATE | IN_MODIFY | IN_MOVED_TO | IN_DELETE | IN_CLOSE_WRITE;

	// Add watch on the path
	_watch_fd = inotify_add_watch(_inotify_fd, path.c_str(), mask);
	if (_watch_fd < 0)
	{
		int norr = errno;
		cleanup_inotify();
		throw RuntimeException(TRACE_INFO,
			"Failed to add inotify watch on \"%s\": %s\n",
			path.c_str(), strerror(norr));
	}

	_watch_path = path;
	_event_mask = mask;
}

void FileWatcher::remove_watch()
{
	std::lock_guard<std::mutex> lock(_mtx);
	cleanup_watch();
	cleanup_inotify();
}

std::pair<uint32_t, std::string> FileWatcher::wait_event()
{
	int inotify_fd_copy;

	// Check if watch is active (with lock)
	{
		std::lock_guard<std::mutex> lock(_mtx);
		if (_inotify_fd < 0 || _watch_fd < 0)
		{
			throw RuntimeException(TRACE_INFO,
				"FileWatcher::wait_event() called without active watch\n");
		}
		inotify_fd_copy = _inotify_fd;
	}

	// Buffer for inotify events
	// Must be large enough for at least one event plus the largest possible filename
	char event_buf[sizeof(struct inotify_event) + NAME_MAX + 1];

	while (true)
	{
		// Read without holding lock (this blocks)
		ssize_t len = ::read(inotify_fd_copy, event_buf, sizeof(event_buf));

		if (len < 0)
		{
			// Interrupted by signal or would block (non-blocking mode) - retry
			if (errno == EINTR || errno == EAGAIN)
				continue;

			// If fd was closed from another thread (during shutdown), return gracefully
			// This happens when another thread calls remove_watch() while we're blocked
			if (errno == EBADF || errno == EINVAL)
			{
				// Return a sentinel value indicating watch was removed
				return std::make_pair(0, std::string());
			}

			// Real error
			int norr = errno;
			throw RuntimeException(TRACE_INFO,
				"inotify read failed: %s\n", strerror(norr));
		}

		// Parse the event
		struct inotify_event* event = (struct inotify_event*)event_buf;

		// Extract filename if present (for directory watches)
		std::string filename;
		if (event->len > 0)
			filename = std::string(event->name);

		return std::make_pair(event->mask, filename);
	}
}

bool FileWatcher::poll_and_add_events(const ContainerValuePtr& cvp, int timeout_ms)
{
	int inotify_fd_copy;

	// Check if watch is active (with lock)
	{
		std::lock_guard<std::mutex> lock(_mtx);
		if (_inotify_fd < 0 || _watch_fd < 0)
			return false; // Not watching, signal exit
		inotify_fd_copy = _inotify_fd;
	}

	// Poll for events (without holding lock)
	struct pollfd pfd;
	pfd.fd = inotify_fd_copy;
	pfd.events = POLLIN;

	int ret = poll(&pfd, 1, timeout_ms);
	if (ret < 0)
	{
		if (errno == EINTR)
			return true; // Interrupted, continue watching
		return false; // Error, signal exit
	}

	if (ret == 0)
		return true; // Timeout, continue watching

	// Read inotify events (without holding lock)
	char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event))));
	ssize_t len = read(inotify_fd_copy, buf, sizeof(buf));
	if (len < 0)
	{
		if (errno == EAGAIN || errno == EINTR)
			return true; // Would block or interrupted, continue watching
		return false; // Error or fd closed, signal exit
	}

	if (len == 0)
		return true; // No data, continue watching

	// Process events and add to container
	const struct inotify_event *event;
	for (char *ptr = buf; ptr < buf + len;
	     ptr += sizeof(struct inotify_event) + event->len)
	{
		event = (const struct inotify_event *) ptr;

		// Check for overflow (ignore as requested)
		if (event->mask & IN_Q_OVERFLOW)
			continue;

		// Only process events with filenames
		if (event->len > 0)
		{
			std::string filename(event->name);
			ValuePtr vp = createStringValue(filename);
			cvp->add(vp);
		}
	}

	return true; // Events processed, continue watching
}

void FileWatcher::watch_thread_func(const ContainerValuePtr& cvp, int timeout_ms)
{
	// Poll and add events in a loop until told to stop
	while (poll_and_add_events(cvp, timeout_ms))
	{
		// Continue watching...
	}
	// When poll_and_add_events returns false, exit thread
}

void FileWatcher::start_watching(const std::string& path, const ContainerValuePtr& cvp, int timeout_ms)
{
	{
		std::lock_guard<std::mutex> lock(_mtx);
		// Check if already watching
		if (_watch_thread.joinable())
			throw RuntimeException(TRACE_INFO,
				"FileWatcher already watching - call stop_watching() first\n");
	}

	// Setup watch (add_watch has its own lock)
	add_watch(path);

	// Start background thread
	{
		std::lock_guard<std::mutex> lock(_mtx);
		_watch_thread = std::thread(&FileWatcher::watch_thread_func, this, cvp, timeout_ms);
	}
}

void FileWatcher::stop_watching()
{
	bool should_join = false;

	// Check if thread is running and cleanup (with lock)
	{
		std::lock_guard<std::mutex> lock(_mtx);
		if (_watch_thread.joinable())
		{
			should_join = true;
			cleanup_watch();     // This will cause poll_and_add_events to return false
			cleanup_inotify();   // Close inotify fd
		}
	}

	// Join thread without holding lock (join can block)
	if (should_join)
		_watch_thread.join();
}