lyx_mirror/src/client/client.cpp
2015-05-17 17:27:27 +02:00

736 lines
15 KiB
C++

/**
* \file client.cpp
* This file is part of LyX, the document processor.
* Licence details can be found in the file COPYING.
*
* \author João Luis M. Assirati
* \author Lars Gullik Bjønnes
*
* Full author contact details are available in file CREDITS.
*/
#include <config.h>
#include "support/ConsoleApplication.h"
#include "support/debug.h"
#include "support/FileName.h"
#include "support/FileNameList.h"
#include "support/lstrings.h"
#include "support/Messages.h"
#include "support/unicode.h"
#include <boost/scoped_ptr.hpp>
// getpid(), getppid()
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
// struct timeval
#ifdef HAVE_SYS_TIME_H
# include <sys/time.h>
#endif
// select()
#ifdef HAVE_SYS_SELECT_H
# include <sys/select.h>
#endif
// socket(), connect()
#ifdef HAVE_SYS_SOCKET_H
# include <sys/socket.h>
#endif
#include <sys/un.h>
// fcntl()
#include <fcntl.h>
// strerror()
#include <string.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <string>
#include <vector>
#include <map>
#include <iostream>
using namespace std;
using namespace lyx::support;
using ::boost::scoped_ptr;
namespace lyx {
// Dummy LyXRC support
struct LyXRC {
string icon_set;
} lyxrc;
// Keep the linker happy on Windows
void lyx_exit(int)
{}
// Dummy language support
Messages const & getGuiMessages()
{
static Messages lyx_messages;
return lyx_messages;
}
Messages const & getMessages(string const &)
{
return getGuiMessages();
}
namespace support {
string itoa(unsigned int i)
{
char buf[20];
sprintf(buf, "%d", i);
return buf;
}
/// Returns the absolute pathnames of all lyx local sockets in
/// file system encoding.
/// Parts stolen from lyx::support::DirList().
FileNameList lyxSockets(string const & dir, string const & pid)
{
FileNameList dirlist;
FileName dirpath(dir + "/");
if (!dirpath.exists() || !dirpath.isDirectory()) {
lyxerr << dir << " does not exist or is not a directory."
<< endl;
return dirlist;
}
FileNameList dirs = dirpath.dirList("");
FileNameList::const_iterator it = dirs.begin();
FileNameList::const_iterator end = dirs.end();
for (; it != end; ++it) {
if (!it->isDirectory())
continue;
string const tmpdir = it->absFileName();
if (!contains(tmpdir, "lyx_tmpdir" + pid))
continue;
FileName lyxsocket(tmpdir + "/lyxsocket");
if (lyxsocket.exists())
dirlist.push_back(lyxsocket);
}
return dirlist;
}
namespace socktools {
/// Connect to the socket \p name.
int connect(FileName const & name)
{
int fd; // File descriptor for the socket
sockaddr_un addr; // Structure that hold the socket address
string const encoded = name.toFilesystemEncoding();
// char sun_path[108]
string::size_type len = encoded.size();
if (len > 107) {
cerr << "lyxclient: Socket address '" << name
<< "' too long." << endl;
return -1;
}
// Synonims for AF_UNIX are AF_LOCAL and AF_FILE
addr.sun_family = AF_UNIX;
encoded.copy(addr.sun_path, 107);
addr.sun_path[len] = '\0';
if ((fd = ::socket(PF_UNIX, SOCK_STREAM, 0))== -1) {
cerr << "lyxclient: Could not create socket descriptor: "
<< strerror(errno) << endl;
return -1;
}
if (::connect(fd,
reinterpret_cast<struct sockaddr *>(&addr),
sizeof(addr)) == -1) {
cerr << "lyxclient: Could not connect to socket " << name.absFileName()
<< ": " << strerror(errno) << endl;
::close(fd);
return -1;
}
if (::fcntl(fd, F_SETFL, O_NONBLOCK) == -1) {
cerr << "lyxclient: Could not set O_NONBLOCK for socket: "
<< strerror(errno) << endl;
::close(fd);
return -1;
}
return fd;
}
} // namespace socktools
} // namespace support
/////////////////////////////////////////////////////////////////////
//
// IOWatch
//
/////////////////////////////////////////////////////////////////////
class IOWatch {
public:
IOWatch();
void clear();
void addfd(int);
bool wait(double);
bool wait();
bool isset(int fd);
private:
fd_set des;
fd_set act;
};
IOWatch::IOWatch()
{
clear();
}
void IOWatch::clear()
{
FD_ZERO(&des);
}
void IOWatch::addfd(int fd)
{
FD_SET(fd, &des);
}
bool IOWatch::wait(double timeout)
{
timeval to;
to.tv_sec = static_cast<long int>(timeout);
to.tv_usec = static_cast<long int>((timeout - to.tv_sec)*1E6);
act = des;
return select(FD_SETSIZE, &act,
(fd_set *)0, (fd_set *)0, &to);
}
bool IOWatch::wait()
{
act = des;
return select(FD_SETSIZE, &act,
(fd_set *)0, (fd_set *)0, (timeval *)0);
}
bool IOWatch::isset(int fd)
{
return FD_ISSET(fd, &act);
}
/////////////////////////////////////////////////////////////////////
//
// LyXDataSocket
//
/////////////////////////////////////////////////////////////////////
// Modified LyXDataSocket class for use with the client
class LyXDataSocket {
public:
LyXDataSocket(FileName const &);
~LyXDataSocket();
// File descriptor of the connection
int fd() const;
// Connection status
bool connected() const;
// Line buffered input from the socket
bool readln(string &);
// Write the string + '\n' to the socket
void writeln(string const &);
private:
// File descriptor for the data socket
int fd_;
// True if the connection is up
bool connected_;
// buffer for input data
string buffer;
};
LyXDataSocket::LyXDataSocket(FileName const & address)
{
if ((fd_ = socktools::connect(address)) == -1) {
connected_ = false;
} else {
connected_ = true;
}
}
LyXDataSocket::~LyXDataSocket()
{
::close(fd_);
}
int LyXDataSocket::fd() const
{
return fd_;
}
bool LyXDataSocket::connected() const
{
return connected_;
}
// Returns true if there was a complete line to input
// A line is of the form <key>:<value>
// A line not of this form will not be passed
// The line read is split and stored in 'key' and 'value'
bool LyXDataSocket::readln(string & line)
{
int const charbuf_size = 100;
char charbuf[charbuf_size]; // buffer for the ::read() system call
int count;
string::size_type pos;
// read and store characters in buffer
while ((count = ::read(fd_, charbuf, charbuf_size - 1)) > 0) {
charbuf[count] = '\0'; // turn it into a c string
buffer += charbuf;
}
// Error conditions. The buffer must still be
// processed for lines read
if (count == 0) { // EOF -- connection closed
connected_ = false;
} else if ((count == -1) && (errno != EAGAIN)) { // IO error
cerr << "lyxclient: IO error." << endl;
connected_ = false;
}
// Cut a line from buffer
if ((pos = buffer.find('\n')) == string::npos)
return false; // No complete line stored
line = buffer.substr(0, pos);
buffer = buffer.substr(pos + 1);
return true;
}
// Write a line of the form <key>:<value> to the socket
void LyXDataSocket::writeln(string const & line)
{
string linen(line + '\n');
int size = linen.size();
int written = ::write(fd_, linen.c_str(), size);
if (written < size) { // Allways mean end of connection.
if ((written == -1) && (errno == EPIPE)) {
// The program will also receive a SIGPIPE
// that must be catched
cerr << "lyxclient: connection closed while writing."
<< endl;
} else {
// Anything else, including errno == EAGAIN, must be
// considered IO error. EAGAIN should never happen
// when line is small
cerr << "lyxclient: IO error: " << strerror(errno);
}
connected_ = false;
}
}
/////////////////////////////////////////////////////////////////////
//
// CmdLineParser
//
/////////////////////////////////////////////////////////////////////
class CmdLineParser {
public:
typedef int (*optfunc)(vector<docstring> const & args);
map<string, optfunc> helper;
map<string, bool> isset;
bool parse(int, char * []);
vector<char *> nonopt;
};
bool CmdLineParser::parse(int argc, char * argv[])
{
int opt = 1;
while (opt < argc) {
vector<docstring> args;
if (helper[argv[opt]]) {
isset[argv[opt]] = true;
int arg = opt + 1;
while ((arg < argc) && (!helper[argv[arg]])) {
args.push_back(from_local8bit(argv[arg]));
++arg;
}
int taken = helper[argv[opt]](args);
if (taken == -1)
return false;
opt += 1 + taken;
} else {
if (argv[opt][0] == '-') {
if ((argv[opt][1] == '-')
&& (argv[opt][2]== '\0')) {
++opt;
while (opt < argc) {
nonopt.push_back(argv[opt]);
++opt;
}
return true;
} else {
cerr << "lyxclient: unknown option "
<< argv[opt] << endl;
return false;
}
}
nonopt.push_back(argv[opt]);
++opt;
}
}
return true;
}
// ~Class CmdLineParser -------------------------------------------------------
namespace cmdline {
docstring mainTmp(from_ascii("/tmp"));
class StopException : public exception
{
public:
StopException(int status) : status_(status) {}
int status() const { return status_; }
private:
int status_;
};
void usage()
{
cerr <<
"Usage: lyxclient [options]\n"
"Options are:\n"
" -a address set address of the lyx socket\n"
" -t directory set system temporary directory (for detecting sockets)\n"
" -p pid select a running lyx by pidi\n"
" -c command send a single command and quit (LYXCMD prefix needed)\n"
" -g file row send a command to go to file and row\n"
" -n name set client name\n"
" -h name display this help end exit\n"
"If -a is not used, lyxclient will use the arguments of -t and -p to look for\n"
"a running lyx. If -t is not set, 'directory' defaults to the system directory. If -p is set,\n"
"lyxclient will connect only to a lyx with the specified pid. Options -c and -g\n"
"cannot be set simultaneoulsly. If no -c or -g options are given, lyxclient\n"
"will read commands from standard input and disconnect when command read is BYE:\n"
"\n"
"System directory is: " << to_utf8(cmdline::mainTmp)
<< endl;
}
int h(vector<docstring> const &)
{
usage();
throw StopException(EXIT_SUCCESS);
}
docstring clientName =
from_ascii(itoa(::getppid()) + ">" + itoa(::getpid()));
int n(vector<docstring> const & arg)
{
if (arg.empty()) {
cerr << "lyxclient: The option -n requires 1 argument."
<< endl;
return -1;
}
clientName = arg[0];
return 1;
}
docstring singleCommand;
int c(vector<docstring> const & arg)
{
if (arg.empty()) {
cerr << "lyxclient: The option -c requires 1 argument."
<< endl;
return -1;
}
singleCommand = arg[0];
return 1;
}
int g(vector<docstring> const & arg)
{
if (arg.size() < 2) {
cerr << "lyxclient: The option -g requires 2 arguments."
<< endl;
return -1;
}
singleCommand = "LYXCMD:server-goto-file-row "
+ arg[0] + ' '
+ arg[1];
return 2;
}
// empty if LYXSOCKET is not set in the environment
docstring serverAddress;
int a(vector<docstring> const & arg)
{
if (arg.empty()) {
cerr << "lyxclient: The option -a requires 1 argument."
<< endl;
return -1;
}
// -a supercedes LYXSOCKET environment variable
serverAddress = arg[0];
return 1;
}
int t(vector<docstring> const & arg)
{
if (arg.empty()) {
cerr << "lyxclient: The option -t requires 1 argument."
<< endl;
return -1;
}
mainTmp = arg[0];
return 1;
}
string serverPid; // Init to empty string
int p(vector<docstring> const & arg)
{
if (arg.empty()) {
cerr << "lyxclient: The option -p requires 1 argument."
<< endl;
return -1;
}
serverPid = to_ascii(arg[0]);
return 1;
}
} // namespace cmdline
/// The main application class
class LyXClientApp : public ConsoleApplication
{
public:
LyXClientApp(int & argc, char * argv[])
: ConsoleApplication("client" PROGRAM_SUFFIX, argc, argv),
argc_(argc), argv_(argv)
{
}
void doExec()
{
try {
int const exit_status = run();
exit(exit_status);
}
catch (cmdline::StopException & e) {
exit(e.status());
}
}
private:
int run();
int & argc_;
char ** argv_;
};
int LyXClientApp::run()
{
// qt changes this, and our numeric conversions require the C locale
setlocale(LC_NUMERIC, "C");
// Set defaults
char const * const lyxsocket = getenv("LYXSOCKET");
if (lyxsocket)
cmdline::serverAddress = from_local8bit(lyxsocket);
// Default temporary
cmdline::mainTmp = FileName::tempPath().absoluteFilePath();
// Command line builder
CmdLineParser args;
args.helper["-h"] = cmdline::h;
args.helper["-c"] = cmdline::c;
args.helper["-g"] = cmdline::g;
args.helper["-n"] = cmdline::n;
args.helper["-a"] = cmdline::a;
args.helper["-t"] = cmdline::t;
args.helper["-p"] = cmdline::p;
// Command line failure conditions:
if ((!args.parse(argc_, argv_))
|| (args.isset["-c"] && args.isset["-g"])
|| (args.isset["-a"] && args.isset["-p"])) {
cmdline::usage();
return EXIT_FAILURE;
}
scoped_ptr<LyXDataSocket> server;
if (!cmdline::serverAddress.empty()) {
server.reset(new LyXDataSocket(FileName(to_utf8(cmdline::serverAddress))));
if (!server->connected()) {
cerr << "lyxclient: " << "Could not connect to "
<< to_utf8(cmdline::serverAddress) << endl;
return EXIT_FAILURE;
}
} else {
// We have to look for an address.
// serverPid can be empty.
FileNameList addrs = lyxSockets(to_filesystem8bit(cmdline::mainTmp), cmdline::serverPid);
FileNameList::const_iterator addr = addrs.begin();
FileNameList::const_iterator end = addrs.end();
for (; addr != end; ++addr) {
// Caution: addr->string() is in filesystem encoding
server.reset(new LyXDataSocket(*addr));
if (server->connected())
break;
lyxerr << "lyxclient: " << "Could not connect to "
<< addr->absFileName() << endl;
}
if (addr == end) {
lyxerr << "lyxclient: No suitable server found."
<< endl;
return EXIT_FAILURE;
}
cerr << "lyxclient: " << "Connected to " << addr->absFileName() << endl;
}
int const serverfd = server->fd();
IOWatch iowatch;
iowatch.addfd(serverfd);
// Used to read from server
string answer;
// Send greeting
server->writeln("HELLO:" + to_utf8(cmdline::clientName));
// wait at most 2 seconds until server responds
iowatch.wait(2.0);
if (iowatch.isset(serverfd) && server->readln(answer)) {
if (prefixIs(answer, "BYE:")) {
cerr << "lyxclient: Server disconnected." << endl;
cout << answer << endl;
return EXIT_FAILURE;
}
} else {
cerr << "lyxclient: No answer from server." << endl;
return EXIT_FAILURE;
}
if (args.isset["-g"] || args.isset["-c"]) {
server->writeln(to_utf8(cmdline::singleCommand));
iowatch.wait(2.0);
if (iowatch.isset(serverfd) && server->readln(answer)) {
cout << answer;
if (prefixIs(answer, "ERROR:"))
return EXIT_FAILURE;
return EXIT_SUCCESS;
} else {
cerr << "lyxclient: No answer from server." << endl;
return EXIT_FAILURE;
}
}
// Take commands from stdin
iowatch.addfd(0); // stdin
bool saidbye = false;
while ((!saidbye) && server->connected()) {
iowatch.wait();
if (iowatch.isset(0)) {
string command;
getline(cin, command);
if (command.empty())
continue;
if (command == "BYE:") {
server->writeln("BYE:");
saidbye = true;
} else {
server->writeln("LYXCMD:" + command);
}
}
if (iowatch.isset(serverfd)) {
while(server->readln(answer))
cout << answer << endl;
}
}
return EXIT_SUCCESS;
}
} // namespace lyx
int main(int argc, char * argv[])
{
lyx::lyxerr.setStream(cerr);
lyx::LyXClientApp app(argc, argv);
return app.exec();
}
namespace boost {
void assertion_failed(char const* a, char const* b, char const* c, long d)
{
lyx::lyxerr << "Assertion failed: " << a << ' ' << b << ' ' << c << ' '
<< d << '\n';
}
} // namespace boost