OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-sensory / opencog / atoms / filedir-v0 / FileSysStream.cc

453 lines · cc ref:
/*
 * opencog/atoms/sensory-v0/FileSysStream.cc
 *
 * Copyright (C) 2020 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/StringValue.h>
#include <opencog/atoms/value/ValueFactory.h>

#include <opencog/sensory-v0/types/atom_types.h>
#include "FileSysStream.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.

// Get the URL string from the sensory node.
// In principle, `senso` should be a SensoryNode, but for the
// crrent round of demos, we relax this and accept anything
// that is a string. This should be reverted, someday.
static std::string get_url_string(const Handle& senso)
{
	ValuePtr relax = senso;
	if (senso->is_executable())
		relax = senso->execute();

	if (not relax->is_type(SENSORY_NODE))
		fprintf(stderr, "Caution: using relaxed stream creation spec %s\n",
			relax->to_string().c_str());

	if (relax->is_type(LINK_VALUE))
		relax = LinkValueCast(relax)->value()[0];

	if (relax->is_type(STRING_VALUE))
		return StringValueCast(relax)->value()[0];

	if (relax->is_node())
		return HandleCast(relax)->get_name();

	throw RuntimeException(TRACE_INFO,
		"Unable to get sring from %s\n", senso->to_string().c_str());
}

FileSysStream::FileSysStream(const Handle& sensor)
	: OutputStream(FILE_SYS_STREAM)
{
	// Get the URL now. Perhaps execution
	// can be or should be defered till later?
	init(get_url_string(sensor));
}

FileSysStream::FileSysStream(void)
	: OutputStream(FILE_SYS_STREAM)
{
	do_describe();
}

FileSysStream::~FileSysStream()
{
}

/// 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 FileSysStream::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;
	do_describe();

#if LATER
	// Ignore the first 7 chars "file://"
	const char* fpath = url.substr(7).c_str();
	_fh = fopen(fpath, "a+");

	if (nullptr == _fh)
	{
		int norr = errno;
		char buff[80];
		buff[0] = 0;
		// Apparently, we are getting the Gnu version of strerror_r
		// and not the XSI version. I suppose it doesn't matter.
		char * ers = strerror_r(norr, buff, 80);
		throw RuntimeException(TRACE_INFO,
			"Unable to open URL \"%s\"\nError was \"%s\"\n",
			url.c_str(), ers);
	}
#endif
}

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

Handle _global_desc = Handle::UNDEFINED;

// 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 FileSysStream::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);

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

	_global_desc = createLink(cmds, CHOICE_LINK);
}

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

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

// This will read one line from the file stream, and return that line.
// So, a line-oriented, buffered interface. For now.
void FileSysStream::update() const
{
	// if (nullptr == _fh) { _value.clear(); return; }
}

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

// ==============================================================
// Process a command.
ValuePtr FileSysStream::write_out(AtomSpace* as, bool silent,
                                  const Handle& cmdref)
{
	Handle cref = cmdref;
	if (cref->is_link())
	{
		if (0 == cref->size())
			throw RuntimeException(TRACE_INFO,
				"Expecting a non-empty list: %s", cref->to_string().c_str());
		cref = cref->getOutgoingAtom(0);
	}
	if (not cref->is_node())
		throw RuntimeException(TRACE_INFO,
			"Expecting a Node: %s", cref->to_string().c_str());

	const std::string& cmd = cref->get_name();
	if (0 == cmd.compare("pwd"))
	{
		return createStringValue(_cwd);
	}

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

		ValueSeq vents;
		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 commans
			closedir(dir);
			throw RuntimeException(TRACE_INFO,
				"Unknown command \"%s\"\n", cmd.c_str());
		}
		closedir(dir);
		return createLinkValue(vents);
	}

	// Commands taking a single argument; in all cases, it *must*.
	// be a file URL, presumably obtained previously with `ls`.
	cref = cmdref;
	if (not cref->is_link() or cref->size() != 2)
		throw RuntimeException(TRACE_INFO,
			"Expecting arguments; got %s", cref->to_string().c_str());

	const Handle& arg1 = cref->getOutgoingAtom(1);
	std::string fpath = get_url_string(arg1);

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

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

	// 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;
		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);
		return createLinkValue(vents);
	}

	if (0 == cmd.compare("magic"))
	{
	}

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

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

// Adds factory when library is loaded.
DEFINE_VALUE_FACTORY(FILE_SYS_STREAM, createFileSysStream)
DEFINE_VALUE_FACTORY(FILE_SYS_STREAM, createFileSysStream, Handle)