/* * LSCP Shell * * Copyright (c) 2014 - 2020 Christian Schoenebeck * * This program is part of LinuxSampler and released under the same terms. */ #include "LSCPClient.h" #include "lscp.h" #include /** * This function is called by the LSCPClient receiving thread to identify * whether the received line is a) just a single line response or b) the first * line of a multi line response. LSCPClient will provide this information via * its multiLine() and messageComplete() methods, which are accessed by the * application's main thread to find out whether it should wait until all lines * of the multi line message were completely received. This strategy (to let * the main app thread wait until a multi line message is really entirely * received) is preferable since it would otherwise require a very complex code * with a bunch of state variables in the main apps loop, which would also make * it very error prone. * * @param line - input: the input line to check * @param skipLine - output: whether @a line shall be silently ignored * @returns true if @a line indicates a multi line response */ static bool _lscpLineIndicatesMultiLineResponse(const String& line, bool& skipLine) { static const String multiLineKey = LSCP_SHK_EXPECT_MULTI_LINE; static const String lscpRefDocKey = "SHD:1"; if (line.substr(0, multiLineKey.length()) == multiLineKey) { skipLine = true; return true; } if (line.substr(0, lscpRefDocKey.length()) == lscpRefDocKey) { skipLine = false; return true; } skipLine = false; return false; } /** @brief Default constructor. * * Initializes a yet unconnected LSCP client. You need to call connect() * afterwards to actually let it connect to some LSCP server for being able to * use the client for something useful. */ LSCPClient::LSCPClient() : Thread(false, false, 1, -1), hSocket(-1), m_callback(NULL), m_errorCallback(NULL), m_multiLineExpected(false) { } /** @brief Destructor. * * Disconnects this client automatically if it is currently still connected to * some LSCP server. */ LSCPClient::~LSCPClient() { disconnect(); } /** * Let this client connect to a (already running) LSCP server listening on * host name @a host and TCP port number @a port. If a connection was * successfully established, this method will also start a thread which will * constantly wait for receiving new data sent by the LSCP server. This thread * will store all received data in an internal buffer which can then be pulled * out with popLine(). * * @param host - host name of LSCP server * @param port - TCP port number of LSCP server * @returns true if connection was successful, false on errors. */ bool LSCPClient::connect(String host, int port) { m_lineBuffer.clear(); m_lines.clear(); // resolve given host name hostent* server = ::gethostbyname(host.c_str()); if (!server) { std::cerr << "Error: Could not resolve host \"" << host << "\".\n"; return false; } // create local TCP socket hSocket = ::socket(AF_INET, SOCK_STREAM, 0); if (hSocket < 0) { std::cerr << "Error: Could not create local socket.\n"; return false; } // TCP connect to server sockaddr_in addr; bzero((char*)&addr, sizeof(addr)); addr.sin_family = AF_INET; bcopy((char*)server->h_addr, (char*)&addr.sin_addr.s_addr, server->h_length); addr.sin_port = htons(port); if (::connect(hSocket, (sockaddr*)&addr, sizeof(addr)) < 0) { std::cerr << "Error: Could not connect to host \"" << host << "\".\n"; std::cerr << "Is linuxsampler running and listening on port " << port << " ?\n"; disconnect(); return false; } StartThread(); return true; // success } /** * Disconnect this client from the currently connected LSCP server. If there * is no connection, then this method does nothing. */ void LSCPClient::disconnect() { if (hSocket >= 0) { StopThread(); ::close(hSocket); hSocket = -1; } } /** * Returns true if this LSCP client is currently connected to a LSCP server. */ bool LSCPClient::isConnected() const { return hSocket >= 0; } /** * Send one byte given by @a c to the connected LSCP server asynchronously. * * @param c - byte (character) to be sent */ bool LSCPClient::send(char c) { String s; s += c; return send(s); } /** * Send a sequence of bytes given by @a s to the connected LSCP server * asynchronously. * * @param s - sequence of bytes (string) to be sent */ bool LSCPClient::send(String s) { //lscpLog("[send] '%s'\n", s.c_str()); if (!isConnected()) return false; int n = (int) ::write(hSocket, &s[0], s.size()); return n == s.size(); } /** * Send a sequence of bytes given by @a s to the connected LSCP server * synchronously. This method will then block until a response line was * received from the LSCP server and will return that response line as result * to this method. Since this currently assumes a single line response, it * should just be used for sending LSCP commands which will exepct a single * line response from the LSCP server. * * @param s - sequence of bytes (string) to be sent * @returns response of LSCP server */ String LSCPClient::sendCommandSync(String s) { m_linesMutex.Lock(); m_lines.clear(); m_linesMutex.Unlock(); m_sync.Set(true); if (!send(s + "\n")) return ""; m_sync.WaitIf(true); m_linesMutex.Lock(); String sResponse = m_lines.back().data; m_lines.clear(); m_linesMutex.Unlock(); return sResponse; } /** * Pulls out and returns the next line received from the LSCP server. If the * return value of this method evaluates to false, then there is currently no * new line received. The line returned will be removed from the internal * receive buffer. If that's not what you want, then use lookAheadLine() * instead. */ optional LSCPClient::popLine() { String s; LockGuard guard(m_linesMutex); if (m_lines.empty()) return optional::nothing; s = m_lines.front().data; m_lines.pop_front(); return s; } /** * Use this method instead of popLine() in case you want to access received * line(s) without removing them from the internal buffer. * * @param index - offset of line to be accessed (0 being the oldest line * received) */ optional LSCPClient::lookAheadLine(int index) { LockGuard guard(m_linesMutex); if (index < 0 || index >= m_lines.size()) return optional::nothing; std::list::iterator it = m_lines.begin(); for (int i = 0; i < index; ++i) ++it; String s = (*it).data; return s; } /** * Returns true if the received line(s), waiting to be pulled out with * popLine(), is/are completely received. So if the next line waiting to be * pulled out with popLine() is a single line response, then this function * returns true. If the next line waiting to be pulled out with popLine() is * considered a part of multi line response though, this function only returns * true if all lines of that expected multi line response were entirely * received already. * * @see multiLine() */ bool LSCPClient::messageComplete() { LockGuard guard(m_linesMutex); if (m_lines.empty()) { //lscpLog("\t[messageComplete false - buffer empty]\n"); return false; } if (!m_lines.front().isMultiLine) { //lscpLog("\t[messageComplete true - single line]\n"); return true; } // // just for debugging purposes ... // int k = 0; // for (std::list::const_iterator it = m_lines.begin(); // it != m_lines.end(); ++it, ++k) // { // lscpLog("\t[messageComplete k=%d : mul=%d cmpl=%d data='%s']\n", k, (*it).isMultiLine, (*it).isMultiLineComplete, (*it).data.c_str()); // } // so next line is part of a multi line response, check if it's complete ... for (std::list::const_iterator it = m_lines.begin(); it != m_lines.end(); ++it) { if ((*it).isMultiLineComplete) return true; } return false; } /** * Returns true if the next line to be pulled out with popLine() is considered * a single line response, it returns false if the next line is a part of a * multi line response instead. In the latter case you should call multiLine() * to check whether all lines of that multi line response were already received * before pulling them out with popLine(). * * @see messageComplete() */ bool LSCPClient::multiLine() { LockGuard guard(m_linesMutex); if (m_lines.empty()) return false; bool bIsMultiLine = m_lines.front().isMultiLine; return bIsMultiLine; } /** * Returns true if new line(s) were received from LSCP server, waiting to be * pulled out with popLine(). */ bool LSCPClient::lineAvailable() const { return !m_lines.empty(); // is thread safe } /** * You may call this method to register a callback function which shall be * notified if new data was received from the connected LSCP server. */ void LSCPClient::setCallback(Callback_t fn) { m_callback = fn; } /** * You may call this method to register a callback function which shall be * notified if some kind of network error occurred. */ void LSCPClient::setErrorCallback(Callback_t fn) { m_errorCallback = fn; } /** * This method is only called by the internal client thread: a call to this * method blocks until a complete new line was received from the LSCP server. */ optional LSCPClient::receiveLine() { if (!isConnected()) return optional::nothing; for (char c; true; ) { int n = (int) ::read(hSocket, &c, 1); if (n < 1) return optional::nothing; if (c == '\r') continue; if (c == '\n') { String s = m_lineBuffer; m_lineBuffer.clear(); return s; } //printf("->%c\n", c); m_lineBuffer += c; } return optional::nothing; } /** * Client's internal receiving thread's loop. It does nothing else than waiting * for new data sent by LSCP server and puts the received lines into an * internal FIFO buffer. */ int LSCPClient::Main() { #if DEBUG Thread::setNameOfCaller("LSCPClient"); #endif while (true) { optional pLine = receiveLine(); // prevent thread from being cancelled // (e.g. to prevent deadlocks while holding mutex lock(s)) pushCancelable(false); if (pLine) { // if there was a line received ... //lscpLog("[client receiveLine] '%s'\n", pLine->c_str()); String s = *pLine; // check whether this is a multi line data ... bool bSkipLine = false; bool bMultiLineComplete = false; if (!m_multiLineExpected && _lscpLineIndicatesMultiLineResponse(s, bSkipLine)) { m_multiLineExpected = true; } else if (m_multiLineExpected && s.substr(0, 1) == ".") { bMultiLineComplete = true; } // store received line in internal FIFO buffer if (!bSkipLine) { Line l = { s, m_multiLineExpected, bMultiLineComplete }; m_linesMutex.Lock(); m_lines.push_back(l); m_linesMutex.Unlock(); } // if this was the last line of a multi line response, reset // for next run if (m_multiLineExpected && bMultiLineComplete) { m_multiLineExpected = false; } if (m_sync.GetUnsafe()) m_sync.Set(false); else if (m_callback) (*m_callback)(this); } else if (m_errorCallback) (*m_errorCallback)(this); // now allow thread being cancelled again // (since all mutexes are now unlocked) popCancelable(); TestCancel(); } return 0; // just to avoid a warning with some old compilers }