OverviewFilesCommitsIssuesDocsReleasesPeers

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

518 lines · cc ref:
/*
 * opencog/atoms/filedir/FileSysNode.cc
 *
 * Copyright (C) 2020,2024,2025 Linas Vepstas
 * 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 <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h> // for strerror()
#include <sys/stat.h>
#include <sys/types.h>

#include <opencog/util/exceptions.h>
#include <opencog/util/oc_assert.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/atoms/base/Link.h>
#include <opencog/atoms/base/Node.h>
#include <opencog/atoms/value/FloatValue.h>
#include <opencog/atoms/value/LinkValue.h>
#include <opencog/atoms/value/QueueValue.h>
#include <opencog/atoms/value/StringValue.h>
#include <opencog/atoms/value/UnisetValue.h>
#include <opencog/atoms/value/VoidValue.h>
#include <opencog/atoms/value/ValueFactory.h>

#include <opencog/sensory/types/atom_types.h>
#include "FileSysNode.h"

using namespace opencog;

/// Under construction.
/// The implementation here is attempting to follow the ideas sketched
/// in opencog/atoms/irc/README.md
///
/// Descriptions of a few basic connectors are given.
/// The rest have not been described.
///
/// There are half-a-dozen commands implemented, enough to
/// make this stream semi-usable in a pseudo "real-world" app.

FileSysNode::FileSysNode(const std::string&& url)
	: TextStreamNode(FILE_SYS_NODE, std::move(url)),
	  _watcher()
{
	init(get_name());
}

FileSysNode::FileSysNode(Type t, const std::string&& url)
	: TextStreamNode(t, std::move(url)),
	  _watcher()
{
	if (not nameserver().isA(t, FILE_SYS_NODE))
		throw RuntimeException(TRACE_INFO,
			"Expecting a FileSysNode, got %d %s\n",
			t, nameserver().getTypeName(t).c_str());

	init(get_name());
}

FileSysNode::~FileSysNode()
{
	_watcher.stop_watching();
}

/// Attempt to open the URL for reading and writing.
/// The URL format is described in
/// https://en.wikipedia.org/wiki/File_URI_scheme
/// and we adhere to that.
///
/// URI formats are:
/// file:/path       ; Not currently supported
/// file:///path     ; Yes, use this
/// file://host/path ; Not currently supported
/// file://./path    ; Dot means localhost
///
/// Possible extensions:
/// file:mode//...
/// where mode is one of the modes described in `man 3 fopen`

static const std::string _prefix("file://");
static const size_t _pfxlen = _prefix.size();

void FileSysNode::init(const std::string& url)
{
	if (0 != url.compare(0, 8, "file:///"))
		throw RuntimeException(TRACE_INFO,
			"Unsupported URL \"%s\"\n", url.c_str());

	_cwd = url;
}

void FileSysNode::open(const ValuePtr& vp)
{
	TextStreamNode::open(vp);

	if (_cvp)
		_cvp->close();

	_cvp = createQueueValue();
}

bool FileSysNode::connected(void) const
{
	return nullptr != _cvp;
}

ValuePtr FileSysNode::stream(void) const
{
	if (not connected()) return createVoidValue();
	return _cvp;
}

void FileSysNode::close(const ValuePtr& vp)
{
	// Stop watching
	_watcher.stop_watching();

	if (_cvp)
	{
		_cvp->close();
		_cvp = nullptr;
	}
}

void FileSysNode::do_write(const std::string& str)
{
	OC_ASSERT(false, "Error: this method should never be called.");
}

// ==============================================================

Handle _global_desc = Handle::UNDEFINED;

#if LATER
// TODO: The below lists only enough of the commands to do the
// basic wiring diagram work. There are additional commands,
// supported i an ad hoc manner, so that the motor demos can
// be developed. The motor demos are kind of independent of the
// wiring demos (for now).
void FileSysNode::do_describe(void)
{
	if (_global_desc) return;

	HandleSeq cmds;

	// List files
	Handle ls_cmd =
		createLink(SECTION,
			createNode(ITEM_NODE, "the ls command"),
			createLink(CONNECTOR_SEQ,
				createLink(CONNECTOR,
					createNode(SEX_NODE, "command"),
					createNode(TYPE_NODE, "WriteLink")),
				createLink(CONNECTOR,
					createNode(SEX_NODE, "command"),
					createNode(ITEM_NODE, "ls")),
				createLink(CONNECTOR,
					createNode(SEX_NODE, "reply"),
					createLink(LINK_SIGNATURE_LINK,
						createNode(TYPE_NODE, "LinkValue"),
						createNode(TYPE_NODE, "StringValue")))));
	cmds.emplace_back(ls_cmd);

	Handle pwd_cmd =
		createLink(SECTION,
			createNode(ITEM_NODE, "the pwd command"),
			createLink(CONNECTOR_SEQ,
				createLink(CONNECTOR,
					createNode(SEX_NODE, "command"),
					createNode(TYPE_NODE, "WriteLink")),
				createLink(CONNECTOR,
					createNode(SEX_NODE, "command"),
					createNode(ITEM_NODE, "pwd")),
				createLink(CONNECTOR,
					createNode(SEX_NODE, "reply"),
					createLink(LINK_SIGNATURE_LINK,
						createNode(TYPE_NODE, "LinkValue"),
						createNode(TYPE_NODE, "StringValue")))));
	cmds.emplace_back(pwd_cmd);

	Handle cd_cmd =
		createLink(SECTION,
			createNode(ITEM_NODE, "the cd command"),
			createLink(CONNECTOR_SEQ,
				createLink(CONNECTOR,
					createNode(SEX_NODE, "command"),
					createNode(TYPE_NODE, "WriteLink")),
				createLink(CONNECTOR,
					createNode(SEX_NODE, "command"),
					createNode(ITEM_NODE, "cd")),
				createLink(CONNECTOR,
					createNode(SEX_NODE, "command"),
					createNode(TYPE_NODE, "SensoryNode")), // XXX maybe??
				createLink(CONNECTOR,
					createNode(SEX_NODE, "reply"),
					createLink(LINK_SIGNATURE_LINK,
						createNode(TYPE_NODE, "LinkValue"),
						createNode(TYPE_NODE, "StringValue")))));
	cmds.emplace_back(cd_cmd);

	Handle mkdir_cmd =
		createLink(SECTION,
			createNode(ITEM_NODE, "the mkdir command"));
	cmds.emplace_back(mkdir_cmd);

	_global_desc = createLink(cmds, CHOICE_LINK);
}

// This is totally bogus because it is unused.
// This should be class static member
ValuePtr FileSysNode::describe(AtomSpace* as, bool silent)
{
	if (_description) return as->add_atom(_description);
	_description = as->add_atom(_global_desc);
	return _description;
}
#endif

// ==============================================================
// helpers
static DIR* do_opendir(const std::string& path)
{
	DIR* dir = opendir(path.c_str());

	// XXX FIXME: for now, throw an error if the location cannot
	// be opened. Some better long-term architecture is desired.
	if (nullptr == dir)
	{
		int norr = errno;
		throw RuntimeException(TRACE_INFO,
			"Location %s inaccessible: %s",
			path.c_str(), strerror(norr));
	}
	return dir;
}


static ValuePtr make_stream_dirent(struct dirent* dent,
                                   const ValuePtr& locurl)
{
	std::string ftype = "unknown";
	switch (dent->d_type)
	{
		case DT_BLK: ftype = "block"; break;
		case DT_CHR: ftype = "char"; break;
		case DT_DIR: ftype = "dir"; break;
		case DT_FIFO: ftype = "fifo"; break;
		case DT_LNK: ftype = "lnk"; break;
		case DT_REG: ftype = "reg"; break;
		case DT_SOCK: ftype = "sock"; break;
		default: break;
	}
	ValueSeq vs({locurl});
	vs.emplace_back(createStringValue(ftype));
	return createLinkValue(vs);
}

// ==============================================================
// Dequeue anything perceived

ValuePtr FileSysNode::read(void) const
{
	if (nullptr == _cvp)
		throw RuntimeException(TRACE_INFO,
			"FileSysNode not open: %s\n", to_string().c_str());

	return _cvp->remove();
}

// ==============================================================
// Helpers to get command strings.
// XXX TODO:
// -- If the ValuePtr is executable, then execute it.
// -- If the ValuePtr is a Link or ListValue, check that it's
//    arity two.

static std::string get_string(const ValuePtr& vp)
{
	if (vp->is_type(STRING_VALUE))
		return StringValueCast(vp)->value()[0];

	 if (vp->is_type(NODE))
		return HandleCast(vp)->get_name();

	static const std::string empty_string;
	return empty_string;
}

static std::string get_arg_string(const ValuePtr& vp, int arg)
{
	if (vp->is_link())
		return get_string(HandleCast(vp)->getOutgoingAtom(arg));

	if (vp->is_type(LINK_VALUE))
		return get_string(LinkValueCast(vp)->value()[arg]);

	throw RuntimeException(TRACE_INFO,
		"FileSysNode expecting StringValue, Node, Link or LinkValue: %s\n",
		vp->to_string().c_str());
}

// ==============================================================
// Process a command.

void FileSysNode::write(const ValuePtr& vp)
{
	if (nullptr == _cvp)
		throw RuntimeException(TRACE_INFO,
			"FileSysNode not open: %s\n", to_string().c_str());

	std::string cmd = get_string(vp);

	if (0 == cmd.compare("pwd"))
	{
		ValueSeq vents;
		vents.push_back(vp);
		vents.emplace_back(string_to_type(_cwd));
		_cvp->add(std::move(createLinkValue(std::move(vents))));
		return;
	}

	if (0 == cmd.compare("watch"))
	{
		// Close existing container
		if (_cvp)
			_cvp->close();

		// Create UnisetValue
		_cvp = createUnisetValue();

		// Extract path from file:// URL
		const std::string& path = _cwd.substr(_pfxlen);

		// Start watching in background thread
		_watcher.start_watching(path, _cvp);

		return;
	}

	// Commands without any arguments. These are applied to all
	// files/dirs in the current working dir.
	if (0 < cmd.size())
	{
		const std::string& path = _cwd.substr(_pfxlen);
		DIR* dir = do_opendir(path.c_str());
		int fd = dirfd(dir);

		ValueSeq vents;
		vents.push_back(vp);

		struct dirent* dent = readdir(dir);
		for (; dent; dent = readdir(dir))
		{
			// Vague attempt to avoid infinite loops during recursion.
			// Directory listing is a recursive process, and recursively
			// descending into the "." directory is an infinite loop.
			// In principle, the agent should not do this. In practice,
			// the current agent architecture is not sophisticated
			// enough to handle this case cleanly. So, for now, as a
			// quick hack, disable the "." and ".." directories.
			// This is not enough for the general case, because
			// softlinks can create loops, and we follow softlinks.
			// Nor is this meant to be an inescapable gaol; soft links
			// might send us off into wild territories. For now, just
			// relax and go with the flow. We'll fix problems later.
			// XXX FIXME the problem above, later.
			if (0 == strcmp(dent->d_name, ".")) continue;
			if (0 == strcmp(dent->d_name, "..")) continue;

			ValuePtr locurl = createStringValue(_cwd + "/" + + dent->d_name);

			// Dispatch by command
			if (0 == cmd.compare("ls"))
			{
				vents.emplace_back(locurl);
				continue;
			}

			if (0 == cmd.compare("special"))
			{
				vents.emplace_back(make_stream_dirent(dent, locurl));
				continue;
			}

			// The remaining commands require performing a stat()
			unsigned int mask = STATX_BTIME | STATX_MTIME | STATX_SIZE;
			struct statx statxbuf;
			int rc = statx(fd, dent->d_name, 0, mask, &statxbuf);
			if (rc)
			{
				int norr = errno;
				closedir(dir);
				throw RuntimeException(TRACE_INFO,
					"Location %s error: %s",
					StringValueCast(locurl)->value()[0].c_str(),
					strerror(norr));
			}

			ValueSeq vs({locurl});
			if (0 == cmd.compare("btime"))
			{
				time_t epoch = statxbuf.stx_btime.tv_sec;
				vs.emplace_back(createStringValue(
					ctime(&epoch)));
				vents.emplace_back(createLinkValue(vs));
				continue;
			}

			if (0 == cmd.compare("mtime"))
			{
				time_t epoch = statxbuf.stx_mtime.tv_sec;
				vs.emplace_back(createStringValue(
					ctime(&epoch)));
				vents.emplace_back(createLinkValue(vs));
				continue;
			}

			if (0 == cmd.compare("filesize"))
			{
				vs.emplace_back(createFloatValue(
					(double) statxbuf.stx_size));
				vents.emplace_back(createLinkValue(vs));
				continue;
			}

			// If we are here, its an unknown command
			closedir(dir);
			throw RuntimeException(TRACE_INFO,
				"Unknown command \"%s\"\n", cmd.c_str());
		}
		closedir(dir);
		_cvp->add(std::move(createLinkValue(std::move(vents))));
		return;
	}

	// Commands taking a single argument; in all cases,
	// that argument *must* be a file URL, presumably obtained
	// previously with `ls`.

	cmd = get_arg_string(vp, 0);
	std::string fpath = get_arg_string(vp, 1);

	if (fpath.compare(0, _pfxlen, _prefix))
		throw RuntimeException(TRACE_INFO,
			"Expecting file URL; got %s", fpath.c_str());

	if (0 == cmd.compare("cd"))
	{
		_cwd = fpath;

		ValueSeq vents;
		vents.push_back(vp);
		vents.emplace_back(createStringValue(_cwd));
		_cvp->add(std::move(createLinkValue(std::move(vents))));
		return;
	}

	// Get dirent info for a single directory.
	// XXX borken for files. Needs fixin.
	if (0 == cmd.compare("special"))
	{
		const std::string& path = fpath.substr(_pfxlen);
		DIR* dir = do_opendir(path.c_str());

		ValueSeq vents;
		vents.push_back(vp);

		struct dirent* dent = readdir(dir);
		for (; dent; dent = readdir(dir))
		{
			if (strcmp(dent->d_name, ".")) continue;
			ValuePtr locurl = createStringValue(fpath);
			vents.emplace_back(make_stream_dirent(dent, locurl));
			break;
		}
		closedir(dir);
		_cvp->add(std::move(createLinkValue(std::move(vents))));
		return;
	}

	// File magic. Not implemented.
	if (0 == cmd.compare("magic"))
	{
		ValueSeq vents;
		vents.push_back(vp);

		_cvp->add(std::move(createLinkValue(std::move(vents))));
		return;
	}

	throw RuntimeException(TRACE_INFO,
		"Unknown command \"%s\"\n", cmd.c_str());
}

// ==============================================================

// Adds factory when library is loaded.
DEFINE_NODE_FACTORY(FileSysNode, FILE_SYS_NODE);

// =========================== END OF FILE ======================