/** * \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 #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 "support/unique_ptr.h" // getpid(), getppid() #ifdef HAVE_SYS_TYPES_H # include #endif #ifdef HAVE_UNISTD_H # include #endif // struct timeval #ifdef HAVE_SYS_TIME_H # include #endif // select() #ifdef HAVE_SYS_SELECT_H # include #endif // socket(), connect() #ifdef HAVE_SYS_SOCKET_H # include #endif #include // fcntl() #include // strerror() #include #include #include #include #include #include #include #include #include using namespace std; using namespace lyx::support; 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(&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(timeout); to.tv_usec = static_cast((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 : // 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 : 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 const & args); map helper; map isset; bool parse(int, char * []); vector nonopt; }; bool CmdLineParser::parse(int argc, char * argv[]) { int opt = 1; while (opt < argc) { vector 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 const &) { usage(); throw StopException(EXIT_SUCCESS); } docstring clientName = from_ascii(itoa(::getppid()) + ">" + itoa(::getpid())); int n(vector 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 const & arg) { if (arg.empty()) { cerr << "lyxclient: The option -c requires 1 argument." << endl; return -1; } singleCommand = arg[0]; return 1; } int g(vector 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 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 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 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; } unique_ptr 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