/ src / neovimconnector.cpp
neovimconnector.cpp
  1  #include "neovimconnector.h"
  2  
  3  #include <QFileInfo>
  4  #include <QLocalSocket>
  5  #include <QMetaMethod>
  6  #include <QTcpSocket>
  7  #include <QtGlobal>
  8  
  9  #include "compat.h"
 10  #include "msgpackiodevice.h"
 11  #include "msgpackrequest.h"
 12  #include "neovimconnectorhelper.h"
 13  
 14  namespace NeovimQt {
 15  
 16  /**
 17   * \class NeovimQt::NeovimConnector
 18   * 
 19   * \brief A Connection to a Neovim instance
 20   *
 21   */
 22  
 23  /**
 24   * Create a new Neovim API connection from an open IO device
 25   */
 26  NeovimConnector::NeovimConnector(QIODevice *dev)
 27  :NeovimConnector(new MsgpackIODevice(dev))
 28  {
 29  }
 30  
 31  NeovimConnector::NeovimConnector(MsgpackIODevice *dev)
 32  :QObject(), m_dev(dev)
 33  {
 34  	m_helper = new NeovimConnectorHelper(this);
 35  	qRegisterMetaType<NeovimError>("NeovimError");
 36  	qRegisterMetaType<int64_t>("int64_t");
 37  
 38  	connect(m_dev, &MsgpackIODevice::error,
 39  			this, &NeovimConnector::msgpackError);
 40  	connect(m_dev, &MsgpackIODevice::aboutToClose, this, &NeovimConnector::aboutToClose);
 41  
 42  	m_dev->setParent(this);
 43  
 44  	if ( !m_dev->isOpen() ) {
 45  		return;
 46  	}
 47  	discoverMetadata();
 48  }
 49  
 50  void NeovimConnector::setRequestTimeout(int ms)
 51  {
 52  	this->m_timeout = ms;
 53  }
 54  
 55  /**
 56   * Sets latest error code and message for this connector
 57   */
 58  void NeovimConnector::setError(NeovimError err, const QString& msg)
 59  {
 60  	m_ready = false;
 61  	if (m_error == NoError && err != NoError) {
 62  		m_error = err;
 63  		m_errorString = msg;
 64  		qWarning() << "Neovim fatal error" << m_errorString;
 65  		emit error(m_error);
 66  	} else {
 67  		// Only the first error is raised
 68  		qDebug() << "(Ignored) Neovim fatal error" << msg;
 69  	}
 70  }
 71  
 72  /** Reset error state */
 73  void NeovimConnector::clearError()
 74  {
 75  	m_error = NoError;
 76  	m_errorString = "";
 77  }
 78  
 79  /**
 80   * Called when an error takes place
 81   */ 
 82  NeovimConnector::NeovimError NeovimConnector::errorCause()
 83  {
 84  	return m_error;
 85  }
 86  
 87  /**
 88   * An human readable error message for the last error
 89   */
 90  QString NeovimConnector::errorString()
 91  {
 92  	return m_errorString;
 93  }
 94  
 95  QString NeovimConnector::connectionDescription()
 96  {
 97  	switch (m_ctype) {
 98  		case SpawnedConnection:
 99  			return m_spawnExe + " " + m_spawnArgs.join(" ");
100  		case HostConnection:
101  			return m_connHost + ":" + QString::number(m_connPort);
102  		case SocketConnection:
103  			return m_connSocket;
104  		default:
105  			return "";
106  	}
107  }
108  
109  /**
110   * Returns the channel id used by Neovim to identify this connection
111   */
112  uint64_t NeovimConnector::channel()
113  {
114  	return m_channel;
115  }
116  
117  /**
118   * Request API information from Neovim
119   */
120  void NeovimConnector::discoverMetadata()
121  {
122  	MsgpackRequest *r = m_dev->startRequestUnchecked("vim_get_api_info", 0);
123  	connect(r, &MsgpackRequest::finished,
124  			m_helper, &NeovimConnectorHelper::handleMetadata);
125  	connect(r, &MsgpackRequest::error,
126  			m_helper, &NeovimConnectorHelper::handleMetadataError);
127  	connect(r, &MsgpackRequest::timeout,
128  			this, &NeovimConnector::fatalTimeout);
129  	r->setTimeout(m_timeout);
130  }
131  
132  /**
133   * True if the Neovim instance is ready
134   * @see ready
135   */
136  bool NeovimConnector::isReady()
137  {
138  	return m_ready;
139  }
140  
141  /**
142   * @warning Do not call this before NeovimConnector::ready as been signaled
143   * @see NeovimConnector::isReady
144   */
145  NeovimApi0* NeovimConnector::api0()
146  {
147  	if ( !m_api0 ) {
148  		if (m_api_compat <= 0) {
149  			m_api0 = new NeovimApi0(this);
150  		} else {
151  			qDebug() << "This instance of neovim DOES NOT support api level 0";
152  		}
153  	}
154  	return m_api0;
155  }
156  /**
157   * @warning Do not call this before NeovimConnector::ready as been signaled
158   * @see NeovimConnector::isReady
159   */
160  NeovimApi1* NeovimConnector::api1()
161  {
162  	if ( !m_api1 ) {
163  		if (m_api_compat <= 1 && 1 <= m_api_supported) {
164  			m_api1 = new NeovimApi1(this);
165  		} else {
166  			qDebug() << "This instance of neovim DOES NOT support api level 1";
167  		}
168  	}
169  	return m_api1;
170  }
171  
172  /** For compatibility with older versions */
173  NeovimApi1* NeovimConnector::neovimObject()
174  {
175  	return api1();
176  }
177  
178  /**
179   * @warning Do not call this before NeovimConnector::ready as been signaled
180   * @see NeovimConnector::isReady
181   */
182  NeovimApi2* NeovimConnector::api2()
183  {
184  	if ( !m_api2 ) {
185  		if (m_api_compat <= 2 && 2 <= m_api_supported) {
186  			m_api2 = new NeovimApi2(this);
187  		} else {
188  			qWarning() << "This instance of neovim not support api level 2";
189  		}
190  	}
191  	return m_api2;
192  }
193  
194  /**
195   * @warning Do not call this before NeovimConnector::ready as been signaled
196   * @see NeovimConnector::isReady
197   */
198  NeovimApi3* NeovimConnector::api3()
199  {
200  	if ( !m_api3 ) {
201  		if (m_api_compat <= 3 && 3 <= m_api_supported) {
202  			m_api3 = new NeovimApi3(this);
203  		} else {
204  			qWarning() << "This instance of neovim not support api level 3";
205  		}
206  	}
207  	return m_api3;
208  }
209  
210  /**
211   * @warning Do not call this before NeovimConnector::ready as been signaled
212   * @see NeovimConnector::isReady
213   */
214  NeovimApi4* NeovimConnector::api4()
215  {
216  	if ( !m_api4 ) {
217  		if (m_api_compat <= 4 && 4 <= m_api_supported) {
218  			m_api4 = new NeovimApi4(this);
219  		} else {
220  			qWarning() << "This instance of neovim not support api level 4";
221  		}
222  	}
223  	return m_api4;
224  }
225  
226  /**
227   * @warning Do not call this before NeovimConnector::ready as been signaled
228   * @see NeovimConnector::isReady
229   */
230  NeovimApi5* NeovimConnector::api5()
231  {
232  	if ( !m_api5 ) {
233  		if (m_api_compat <= 5 && 5 <= m_api_supported) {
234  			m_api5 = new NeovimApi5(this);
235  		} else {
236  			qWarning() << "This instance of neovim not support api level 5";
237  		}
238  	}
239  	return m_api5;
240  }
241  
242  /**
243   * @warning Do not call this before NeovimConnector::ready as been signaled
244   * @see NeovimConnector::isReady
245   */
246  NeovimApi6* NeovimConnector::api6()
247  {
248  	if ( !m_api6 ) {
249  		if (m_api_compat <= 6 && 6 <= m_api_supported) {
250  			m_api6 = new NeovimApi6(this);
251  		} else {
252  			qWarning() << "This instance of neovim not support api level 6";
253  		}
254  	}
255  	return m_api6;
256  }
257  
258  /**
259   * Launch an embedded Neovim process
260   * @see processExited
261   */
262  NeovimConnector* NeovimConnector::spawn(const QStringList& params, const QString& exe)
263  {
264  	QProcess *p = new QProcess();
265  	QStringList args;
266  	// Neovim accepts a `--' argument after which only filenames are passed.
267  	// If the user has supplied it, our arguments must appear before.
268  	if (params.indexOf("--") == -1) {
269  		args << "--embed";
270  		args.append(params);
271  	} else {
272  		int idx = params.indexOf("--");
273  		args.append(params.mid(0, idx));
274  		args << "--embed";
275  		args.append(params.mid(idx));
276  	}
277  
278  	NeovimConnector *c = new NeovimConnector(p);
279  	c->m_ctype = SpawnedConnection;
280  	c->m_spawnArgs = params;
281  	c->m_spawnExe = exe;
282  
283  #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
284  	connect(p, SIGNAL(error(QProcess::ProcessError)),
285  			c, SLOT(processError(QProcess::ProcessError)));
286  #else
287  	connect(p, &QProcess::errorOccurred, c, &NeovimConnector::processError);
288  #endif
289  	connect(p, SIGNAL(finished(int,QProcess::ExitStatus)),
290  			c, SIGNAL(processExited(int)));
291  	connect(p, &QProcess::started,
292  			c, &NeovimConnector::discoverMetadata);
293  	p->start(exe, args);
294  	return c;
295  }
296  
297  /**
298   * Connect to Neovim using a local UNIX socket.
299   *
300   * This method also works in Windows, using named pipes.
301   *
302   * @see QLocalSocket
303   */
304  NeovimConnector* NeovimConnector::connectToSocket(const QString& path)
305  {
306  	QLocalSocket *s = new QLocalSocket();
307  	NeovimConnector *c = new NeovimConnector(s);
308  	c->m_ctype = SocketConnection;
309  #if defined(Q_OS_UNIX)
310  	// #936: Qt assumes paths relative to /tmp
311  	// https://bugreports.qt.io/browse/QTBUG-50877
312  	c->m_connSocket = QFileInfo(path).absoluteFilePath();
313  #else
314  	c->m_connSocket = path;
315  #endif
316  
317  #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
318  	connect(s, SIGNAL(error(QLocalSocket::LocalSocketError)),
319  			c, SLOT(socketError()));
320  #else
321  	connect(s, &QLocalSocket::errorOccurred, c, &NeovimConnector::socketError);
322  #endif
323  	connect(s, &QLocalSocket::connected,
324  			c, &NeovimConnector::discoverMetadata);
325  	s->connectToServer(c->m_connSocket);
326  	return c;
327  }
328  
329  /**
330   * Connect to a Neovim through a TCP connection
331   *
332   * @param host is a valid hostname or IP address
333   * @param port is the TCP port
334   */
335  NeovimConnector* NeovimConnector::connectToHost(const QString& host, int port)
336  {
337  	QTcpSocket *s = new QTcpSocket();
338  	NeovimConnector *c = new NeovimConnector(s);
339  
340  	c->m_ctype = HostConnection;
341  	c->m_connHost = host;
342  	c->m_connPort = port;
343  
344  #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
345  	connect(s, SIGNAL(error(QAbstractSocket::SocketError)),
346  			c, SLOT(socketError()));
347  #else
348  	connect(s, &QAbstractSocket::errorOccurred, c, &NeovimConnector::socketError);
349  #endif
350  	connect(s, &QAbstractSocket::connected,
351  			c, &NeovimConnector::discoverMetadata);
352  	s->connectToHost(host, port);
353  	return c;
354  }
355  
356  /**
357   * Connect to a running instance of Neovim (if available).
358   *
359   * This method gets the Neovim endpoint from the NVIM_LISTEN_ADDRESS environment
360   * variable, if it is not available a new Neovim instance is spawned().
361   *
362   * @see spawn()
363   */
364  NeovimConnector* NeovimConnector::connectToNeovim(const QString& server)
365  {
366  	QString addr = server;
367  	if (addr.isEmpty()) {
368  		 addr = QString::fromLocal8Bit(qgetenv("NVIM_LISTEN_ADDRESS"));
369  	}
370  	if (addr.isEmpty()) {
371  		return spawn();
372  	}
373  
374  	int colon_pos = addr.lastIndexOf(':');
375  	if (colon_pos != -1 && colon_pos != 0 && addr[colon_pos-1] != ':') {
376  		bool ok;
377  		int port = midString(addr, colon_pos + 1).toInt(&ok);
378  		if (ok) {
379  			QString host = addr.mid(0, colon_pos);
380  			return connectToHost(host, port);
381  		}
382  	}
383  	return connectToSocket(addr);
384  }
385  
386  NeovimConnector* NeovimConnector::fromStdinOut()
387  {
388  	return new NeovimConnector(MsgpackIODevice::fromStdinOut());
389  }
390  
391  /**
392   * Called when running embedded Neovim to report an error
393   * with the Neovim process
394   */
395  void NeovimConnector::processError(QProcess::ProcessError err)
396  {
397  	switch(err) {
398  	case QProcess::FailedToStart:
399  		setError(FailedToStart, m_dev->errorString());
400  		break;
401  	case QProcess::Crashed:
402  		setError(Crashed, "The Neovim process has crashed");
403  		break;
404  	default:
405  		// In practice we should be able to catch other types of
406  		// errors from the QIODevice
407  		qDebug() << "Neovim process error " << m_dev->errorString();
408  	}
409  }
410  
411  /** Handle errors from QLocalSocket or QTcpSocket */
412  void NeovimConnector::socketError()
413  {
414  	setError(SocketError, m_dev->errorString());
415  }
416  
417  /** Handle errors in MsgpackIODevice */
418  void NeovimConnector::msgpackError()
419  {
420  	setError(MsgpackError, m_dev->errorString());
421  }
422  
423  /**
424   * Raise a fatal error for a Neovim timeout
425   *
426   * Sometimes Neovim takes too long to respond to some requests, or maybe
427   * the channel is stuck. In such cases it is preferable to raise and error,
428   * internally this is what discoverMetadata does if Neovim does not reply.
429   */
430  void NeovimConnector::fatalTimeout()
431  {
432  	setError(RuntimeMsgpackError, "Neovim is taking too long to respond");
433  }
434  
435  void NeovimConnector::setRequestHandler(MsgpackRequestHandler *h)
436  {
437  	m_dev->setRequestHandler(h);
438  }
439  
440  /**
441   * True if NeovimConnector::reconnect can be called to reconnect with Neovim. This
442   * is true unless you built the NeovimConnector ctor directly instead
443   * of using on of the static methods.
444   */
445  bool NeovimConnector::canReconnect()
446  {
447  	return m_ctype != OtherConnection;
448  }
449  
450  /** @see NeovimConnector::NeovimConnectionType */
451  NeovimConnector::NeovimConnectionType NeovimConnector::connectionType()
452  {
453  	return m_ctype;
454  }
455  
456  /**
457   * Create a new connection using the same parameters as the current one.
458   *
459   * This is the equivalent of creating a new object with spawn(), connectToHost(),
460   * or connectToSocket()
461   *
462   * If canReconnect() returns false, this function will return NULL.
463   */
464  NeovimConnector* NeovimConnector::reconnect()
465  {
466  	switch(m_ctype) {
467  	case SpawnedConnection:
468  		return NeovimConnector::spawn(m_spawnArgs, m_spawnExe);
469  	case HostConnection:
470  		return NeovimConnector::connectToHost(m_connHost, m_connPort);
471  	case SocketConnection:
472  		return NeovimConnector::connectToSocket(m_connSocket);
473  	default:
474  		return NULL;
475  	}
476  	// NOT-REACHED
477  	return NULL;
478  }
479  
480  /** The minimum API level supported by this instance */
481  quint64 NeovimConnector::apiCompatibility()
482  {
483  	return m_api_compat;
484  }
485  
486  /** The maximum API level supported by this instance */
487  quint64 NeovimConnector::apiLevel()
488  {
489  	return m_api_supported;
490  }
491  
492  /** Inspect Neovim metadata for ui_option support */
493  bool NeovimConnector::hasUIOption(const QByteArray &option)
494  {
495  	return m_uiOptions.contains(option);
496  }
497  
498  /**
499   * \fn NeovimQt::NeovimConnector::error(NeovimError)
500   *
501   * This signal is emitted when an error occurs. Use NeovimConnector::errorString
502   * to get an error message.
503   */
504  
505  /**
506   * \fn NeovimQt::NeovimConnector::processExited(int exitStatus)
507   *
508   * If the Neovim process was started using NeovimQt::NeovimConnector::spawn this signal
509   * is emitted when the process exits.
510   */
511  
512  } // namespace NeovimQt
513  
514  #include "moc_neovimconnector.cpp"