OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-sensory / opencog / atoms / terminal-v0 / TerminalStream.cc

261 lines · cc ref:
/*
 * opencog/atoms/sensory-v0/TerminalStream.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 <sys/ioctl.h>
#include <asm/termbits.h>

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h> // for strerror()
#include <unistd.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/StringValue.h>
#include <opencog/atoms/value/ValueFactory.h>

#include <opencog/sensory-v0/types/atom_types.h>
#include "TerminalStream.h"

using namespace opencog;

// Terminal I/O using posix_openpt(), ptsname(), grantpt(), and unlockpt()
// ttyname() pts(4), pty(7)

TerminalStream::TerminalStream(Type t, const std::string& str)
	: OutputStream(t)
{
	OC_ASSERT(nameserver().isA(_type, TERMINAL_STREAM),
		"Bad TerminalStream constructor!");
	init();
}

TerminalStream::TerminalStream(void)
	: OutputStream(TERMINAL_STREAM)
{
	init();
}

TerminalStream::TerminalStream(const ValueSeq& seq)
	: OutputStream(TERMINAL_STREAM) // seq
{
	init();
}

TerminalStream::~TerminalStream()
{
	// Runs only if GC runs. This is a problem.
	halt();
}

void TerminalStream::halt(void) const
{
	if (_fh)
		fclose (_fh);
	_fh = nullptr;

	if (_xterm_pid)
		kill (_xterm_pid, SIGKILL);
	_xterm_pid= 0;

	_value.clear();
}

void TerminalStream::init(void)
{
	do_describe();

	_fh = nullptr;

	int fd = posix_openpt(O_RDWR|O_NOCTTY);
	if (0 > fd)
		throw RuntimeException(TRACE_INFO, "Can't open PTY %d %s",
			errno, strerror(errno));

	int rc = unlockpt(fd);
	if (0 != rc)
		throw RuntimeException(TRACE_INFO, "Can't unlock PTY %d %s",
			errno, strerror(errno));

	// Has no effect, but whatever.
	// int arg = TIOCPKT_FLUSHWRITE;
	// rc = ioctl(fd, TIOCPKT, &arg);

	// Get the PTY name
	#define PTSZ 256
	char my_ptsname[PTSZ];
	rc = ptsname_r(fd, my_ptsname, PTSZ);
	if (0 != rc)
		throw RuntimeException(TRACE_INFO, "Can't get PTY name %d %s",
			errno, strerror(errno));

	printf("Opened %s\n", my_ptsname);

	// Build arguments for xterm
	std::string ccn = "-S";
	ccn += my_ptsname;
	ccn += "/" + std::to_string(fd);

	// Insane old-school hackery
	_xterm_pid = fork();
	if (-1 == _xterm_pid)
		throw RuntimeException(TRACE_INFO, "Failed to fork: (%d) %s",
			errno, strerror(errno));

	if (0 == _xterm_pid)
	{
		execl("/usr/bin/xterm", "xterm", ccn.c_str(), (char *) NULL);
		fprintf(stderr, "%s: line %d\n", __FILE__, __LINE__);
		fprintf(stderr, "Failed to start /usr/bin/xterm: (%d) %s\n",
			errno, strerror(errno));
		fprintf(stderr, "Make sure that xterm is installed!\n");
		exit(1);
	}

	printf("Created xterm pid=%d\n", _xterm_pid);

	// Hmm. Seems like the right thing to do is to close the terminal
	// created by open_pt() above, and open another, as a slave.
	// And I guess this works because fd was opened with O_NOCTTY
	// The alternative is `_fh = fdopen(fd, "a+")` but this flakes.
	close(fd);

	_fh = fopen(my_ptsname, "a+");
}

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

Handle _global_desc = Handle::UNDEFINED;

void TerminalStream::do_describe(void)
{
	if (_global_desc) return;

	HandleSeq cmds;

	// Describe exactly how to Open this stream.
	// It needs no special arguments.
	Handle open_cmd =
		make_description("the open terminal command",
		                 "OpenLink", "TerminalStream");
	cmds.emplace_back(open_cmd);

	// Write text
	Handle write_cmd =
		make_description("the write stuff command",
		                 "WriteLink", "ItemNode");
	cmds.emplace_back(write_cmd);

	_global_desc = createLink(cmds, CHOICE_LINK);
}

// This is totally bogus because it is unused.
// This should be class static member
ValuePtr TerminalStream::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.
// This blocks, waiting for input, if there is no input.
void TerminalStream::update() const
{
	if (nullptr == _fh) { _value.clear(); return; }

#define BUFSZ 4080
	char buff[BUFSZ];
	buff[0] = 0;

	// Locking and blocking. There seems to be a feature/bug in some
	// combinations of linux kernel + glibc + xterm that prevents the
	// `xterm-bridge.scm` demo from operating nicely, if plain old
	// fgets() is used. I tried everything, including ioctl(TIOCPKT).
	// The problem seems to be that fgets() grabs some lock, and so
	// when there are two threads using two pipes to talk to two xterm
	// processes, data is not sent/received in one pipe until the
	// other is nudged, momentarily dropping the lock.  You can see
	// the wonky behavior in the demo. Using fgets_unlocked() allows
	// the demo to run "normally", at the cost that sometimes, the
	// same data is returned twice! WTF. I can't tell if this is a
	// feature or a bug or what the heck this is supposed to be.
	// However, the xterm demo is low-priority, so further debugging
	// seems unjustified.
#ifdef _GNU_SOURCE
	char* rd = fgets_unlocked(buff, BUFSZ, _fh);
#else
	char* rd = fgets(buff, BUFSZ, _fh);
#endif

	// nullptr is EOF
	if (nullptr == rd)
	{
		halt();
		return;
	}

	_value.resize(1);
	_value[0] = createStringValue(buff);
}

// ==============================================================
// Write stuff to a file.

void TerminalStream::do_write(const std::string& str)
{
	if (nullptr == _fh)
		throw RuntimeException(TRACE_INFO,
			"Text stream not open\n");

	fprintf(_fh, "%s", str.c_str());
	fflush(_fh);
}

// Write stuff to a file.
ValuePtr TerminalStream::write_out(AtomSpace* as, bool silent,
                                   const Handle& cref)
{
	return do_write_out(as, silent, cref);
}

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

// Adds factory when library is loaded.
DEFINE_VALUE_FACTORY(TERMINAL_STREAM, createTerminalStream)
DEFINE_VALUE_FACTORY(TERMINAL_STREAM, createTerminalStream, ValueSeq)

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

void opencog_sensory_v0_terminal_init(void)
{
   // Force shared lib ctors to run
};