Remote TCP (#557)

* Remote TCP proof of concept

* Allow usage of both tcp and child connections using `TxWrapper`

* Tidy up and add flag to set tcp target

* Add readme section

* Remove the need for `Compat<TxWrapper>` as we are wrapping it anyway
macos-click-through
Benjamin Davies 4 years ago committed by GitHub
parent 799e043ffd
commit d8d6f4eac2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

2
Cargo.lock generated

@ -1577,6 +1577,7 @@ dependencies = [
"euclid", "euclid",
"flexi_logger", "flexi_logger",
"font-kit", "font-kit",
"futures 0.3.12",
"image", "image",
"lazy_static", "lazy_static",
"log", "log",
@ -1585,6 +1586,7 @@ dependencies = [
"neovide-derive", "neovide-derive",
"nvim-rs", "nvim-rs",
"parking_lot 0.10.2", "parking_lot 0.10.2",
"pin-project",
"rand", "rand",
"rmpv", "rmpv",
"rust-embed", "rust-embed",

@ -29,7 +29,7 @@ rmpv = "0.4.4"
rust-embed = { version = "5.2.0", features = ["debug-embed"] } rust-embed = { version = "5.2.0", features = ["debug-embed"] }
image = "0.22.3" image = "0.22.3"
nvim-rs = { git = "https://github.com/kethku/nvim-rs", features = [ "use_tokio" ] } nvim-rs = { git = "https://github.com/kethku/nvim-rs", features = [ "use_tokio" ] }
tokio = { version = "0.2.9", features = [ "blocking", "process", "time" ] } tokio = { version = "0.2.9", features = [ "blocking", "process", "time", "tcp" ] }
async-trait = "0.1.18" async-trait = "0.1.18"
crossfire = "0.1" crossfire = "0.1"
lazy_static = "1.4.0" lazy_static = "1.4.0"
@ -43,6 +43,8 @@ which = "4"
dirs = "2" dirs = "2"
rand = "0.7" rand = "0.7"
skia-safe = "0.32.1" skia-safe = "0.32.1"
pin-project = "0.4.27"
futures = "0.3.12"
[dev-dependencies] [dev-dependencies]
mockall = "0.7.0" mockall = "0.7.0"

@ -64,6 +64,28 @@ Font fallback supports rendering of emoji not contained in the configured font.
Neovide supports displaying a full gui window from inside wsl via the `--wsl` command argument. Communication is passed via standard io into the wsl copy of neovim providing identical experience similar to visual studio code's remote editing https://code.visualstudio.com/docs/remote/remote-overview. Neovide supports displaying a full gui window from inside wsl via the `--wsl` command argument. Communication is passed via standard io into the wsl copy of neovim providing identical experience similar to visual studio code's remote editing https://code.visualstudio.com/docs/remote/remote-overview.
### Remote TCP Support
Neovide supports connecting to a remote instance of Neovim over a TCP socket via the `--remote-tcp` command argument. This would allow you to run Neovim on a remote machine and use the GUI on your local machine, connecting over the network.
Launch Neovim as a TCP server (on port 6666) by running:
```sh
nvim --headless --listen localhost:6666
```
And then connect to it using:
```sh
/path/to/neovide --remote-tcp=localhost:6666
```
By specifying to listen on localhost, you only allow connections from your local computer. If you are actually doing this over a network you will want to use SSH port forwarding for security, and then connect as before.
```sh
ssh -L 6666:localhost:6666 ip.of.other.machine nvim --headless --listen localhost:6666
```
### Some Nonsense ;) ### Some Nonsense ;)
``` ```

@ -0,0 +1,65 @@
//! This module contains adaptations of the functions found in
//! https://github.com/KillTheMule/nvim-rs/blob/master/src/create/tokio.rs
use std::{
io::{self, Error, ErrorKind},
process::Stdio,
};
use tokio::{
io::split,
net::{TcpStream, ToSocketAddrs},
process::Command,
spawn,
task::JoinHandle,
};
use nvim_rs::compat::tokio::TokioAsyncReadCompatExt;
use nvim_rs::{error::LoopError, neovim::Neovim, Handler};
use crate::bridge::{TxWrapper, WrapTx};
/// Connect to a neovim instance via tcp
pub async fn new_tcp<A, H>(
addr: A,
handler: H,
) -> io::Result<(Neovim<TxWrapper>, JoinHandle<Result<(), Box<LoopError>>>)>
where
A: ToSocketAddrs,
H: Handler<Writer = TxWrapper>,
{
let stream = TcpStream::connect(addr).await?;
let (reader, writer) = split(stream);
let (neovim, io) = Neovim::<TxWrapper>::new(reader.compat_read(), writer.wrap_tx(), handler);
let io_handle = spawn(io);
Ok((neovim, io_handle))
}
/// Connect to a neovim instance by spawning a new one
///
/// stdin/stdout will be rewritten to `Stdio::piped()`
pub async fn new_child_cmd<H>(
cmd: &mut Command,
handler: H,
) -> io::Result<(Neovim<TxWrapper>, JoinHandle<Result<(), Box<LoopError>>>)>
where
H: Handler<Writer = TxWrapper>,
{
let mut child = cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?;
let stdout = child
.stdout
.take()
.ok_or_else(|| Error::new(ErrorKind::Other, "Can't open stdout"))?
.compat_read();
let stdin = child
.stdin
.take()
.ok_or_else(|| Error::new(ErrorKind::Other, "Can't open stdin"))?
.wrap_tx();
let (neovim, io) = Neovim::<TxWrapper>::new(stdout, stdin, handler);
let io_handle = spawn(io);
Ok((neovim, io_handle))
}

@ -3,14 +3,14 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use crossfire::mpsc::TxUnbounded; use crossfire::mpsc::TxUnbounded;
use log::trace; use log::trace;
use nvim_rs::{compat::tokio::Compat, Handler, Neovim}; use nvim_rs::{Handler, Neovim};
use parking_lot::Mutex; use parking_lot::Mutex;
use rmpv::Value; use rmpv::Value;
use tokio::process::ChildStdin;
use tokio::task; use tokio::task;
use super::events::{parse_redraw_event, RedrawEvent}; use super::events::{parse_redraw_event, RedrawEvent};
use super::ui_commands::UiCommand; use super::ui_commands::UiCommand;
use crate::bridge::TxWrapper;
use crate::error_handling::ResultPanicExplanation; use crate::error_handling::ResultPanicExplanation;
use crate::settings::SETTINGS; use crate::settings::SETTINGS;
@ -34,13 +34,13 @@ impl NeovimHandler {
#[async_trait] #[async_trait]
impl Handler for NeovimHandler { impl Handler for NeovimHandler {
type Writer = Compat<ChildStdin>; type Writer = TxWrapper;
async fn handle_notify( async fn handle_notify(
&self, &self,
event_name: String, event_name: String,
arguments: Vec<Value>, arguments: Vec<Value>,
_neovim: Neovim<Compat<ChildStdin>>, _neovim: Neovim<TxWrapper>,
) { ) {
trace!("Neovim notification: {:?}", &event_name); trace!("Neovim notification: {:?}", &event_name);

@ -1,5 +1,7 @@
pub mod create;
mod events; mod events;
mod handler; mod handler;
mod tx_wrapper;
mod ui_commands; mod ui_commands;
use std::env; use std::env;
@ -10,7 +12,7 @@ use std::sync::Arc;
use crossfire::mpsc::{RxUnbounded, TxUnbounded}; use crossfire::mpsc::{RxUnbounded, TxUnbounded};
use log::{error, info, warn}; use log::{error, info, warn};
use nvim_rs::{create::tokio as create, UiAttachOptions}; use nvim_rs::UiAttachOptions;
use rmpv::Value; use rmpv::Value;
use tokio::process::Command; use tokio::process::Command;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
@ -20,6 +22,7 @@ use crate::settings::*;
use crate::window::window_geometry_or_default; use crate::window::window_geometry_or_default;
pub use events::*; pub use events::*;
use handler::NeovimHandler; use handler::NeovimHandler;
pub use tx_wrapper::{TxWrapper, WrapTx};
pub use ui_commands::UiCommand; pub use ui_commands::UiCommand;
#[cfg(windows)] #[cfg(windows)]
@ -129,6 +132,22 @@ pub fn create_nvim_command() -> Command {
cmd cmd
} }
enum ConnectionMode {
Child,
RemoteTcp(String),
}
fn connection_mode() -> ConnectionMode {
let tcp_prefix = "--remote-tcp=";
if let Some(arg) = std::env::args().find(|arg| arg.starts_with(tcp_prefix)) {
let input = &arg[tcp_prefix.len()..];
ConnectionMode::RemoteTcp(input.to_owned())
} else {
ConnectionMode::Child
}
}
async fn start_neovim_runtime( async fn start_neovim_runtime(
ui_command_sender: TxUnbounded<UiCommand>, ui_command_sender: TxUnbounded<UiCommand>,
ui_command_receiver: RxUnbounded<UiCommand>, ui_command_receiver: RxUnbounded<UiCommand>,
@ -137,8 +156,10 @@ async fn start_neovim_runtime(
) { ) {
let (width, height) = window_geometry_or_default(); let (width, height) = window_geometry_or_default();
let handler = NeovimHandler::new(ui_command_sender.clone(), redraw_event_sender.clone()); let handler = NeovimHandler::new(ui_command_sender.clone(), redraw_event_sender.clone());
let (mut nvim, io_handler, _) = create::new_child_cmd(&mut create_nvim_command(), handler) let (mut nvim, io_handler) = match connection_mode() {
.await ConnectionMode::Child => create::new_child_cmd(&mut create_nvim_command(), handler).await,
ConnectionMode::RemoteTcp(address) => create::new_tcp(address, handler).await,
}
.unwrap_or_explained_panic("Could not locate or start neovim process"); .unwrap_or_explained_panic("Could not locate or start neovim process");
if nvim.get_api_info().await.is_err() { if nvim.get_api_info().await.is_err() {

@ -0,0 +1,58 @@
use pin_project::pin_project;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::{
io::{AsyncWrite, WriteHalf},
net::TcpStream,
process::ChildStdin,
};
#[pin_project(project = TxProj)]
pub enum TxWrapper {
Child(#[pin] ChildStdin),
Tcp(#[pin] WriteHalf<TcpStream>),
}
impl futures::io::AsyncWrite for TxWrapper {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, io::Error>> {
match self.project() {
TxProj::Child(inner) => inner.poll_write(cx, buf),
TxProj::Tcp(inner) => inner.poll_write(cx, buf),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
match self.project() {
TxProj::Child(inner) => inner.poll_flush(cx),
TxProj::Tcp(inner) => inner.poll_flush(cx),
}
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
match self.project() {
TxProj::Child(inner) => inner.poll_shutdown(cx),
TxProj::Tcp(inner) => inner.poll_shutdown(cx),
}
}
}
pub trait WrapTx {
fn wrap_tx(self) -> TxWrapper;
}
impl WrapTx for ChildStdin {
fn wrap_tx(self) -> TxWrapper {
TxWrapper::Child(self)
}
}
impl WrapTx for WriteHalf<TcpStream> {
fn wrap_tx(self) -> TxWrapper {
TxWrapper::Tcp(self)
}
}

@ -3,9 +3,9 @@ use log::trace;
#[cfg(windows)] #[cfg(windows)]
use log::error; use log::error;
use nvim_rs::compat::tokio::Compat;
use nvim_rs::Neovim; use nvim_rs::Neovim;
use tokio::process::ChildStdin;
use crate::bridge::TxWrapper;
#[cfg(windows)] #[cfg(windows)]
use crate::windows_utils::{ use crate::windows_utils::{
@ -43,7 +43,7 @@ pub enum UiCommand {
} }
impl UiCommand { impl UiCommand {
pub async fn execute(self, nvim: &Neovim<Compat<ChildStdin>>) { pub async fn execute(self, nvim: &Neovim<TxWrapper>) {
match self { match self {
UiCommand::Resize { width, height } => nvim UiCommand::Resize { width, height } => nvim
.ui_try_resize(width.max(10) as i64, height.max(3) as i64) .ui_try_resize(width.max(10) as i64, height.max(3) as i64)

@ -7,12 +7,11 @@ use flexi_logger::{Cleanup, Criterion, Duplicate, Logger, Naming};
mod from_value; mod from_value;
pub use from_value::FromValue; pub use from_value::FromValue;
use log::warn; use log::warn;
use nvim_rs::compat::tokio::Compat;
use nvim_rs::Neovim; use nvim_rs::Neovim;
use parking_lot::RwLock; use parking_lot::RwLock;
pub use rmpv::Value; pub use rmpv::Value;
use tokio::process::ChildStdin;
use crate::bridge::TxWrapper;
use crate::error_handling::ResultPanicExplanation; use crate::error_handling::ResultPanicExplanation;
lazy_static! { lazy_static! {
@ -51,6 +50,7 @@ impl Settings {
false false
} else { } else {
!(arg.starts_with("--geometry=") !(arg.starts_with("--geometry=")
|| arg.starts_with("--remote-tcp=")
|| arg == "--version" || arg == "--version"
|| arg == "-v" || arg == "-v"
|| arg == "--help" || arg == "--help"
@ -129,7 +129,7 @@ impl Settings {
(*value).clone() (*value).clone()
} }
pub async fn read_initial_values(&self, nvim: &Neovim<Compat<ChildStdin>>) { pub async fn read_initial_values(&self, nvim: &Neovim<TxWrapper>) {
let keys: Vec<String> = self.listeners.read().keys().cloned().collect(); let keys: Vec<String> = self.listeners.read().keys().cloned().collect();
for name in keys { for name in keys {
@ -147,7 +147,7 @@ impl Settings {
} }
} }
pub async fn setup_changed_listeners(&self, nvim: &Neovim<Compat<ChildStdin>>) { pub async fn setup_changed_listeners(&self, nvim: &Neovim<TxWrapper>) {
let keys: Vec<String> = self.listeners.read().keys().cloned().collect(); let keys: Vec<String> = self.listeners.read().keys().cloned().collect();
for name in keys { for name in keys {
@ -184,25 +184,24 @@ impl Settings {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use async_trait::async_trait; use async_trait::async_trait;
use nvim_rs::create::tokio as create; use nvim_rs::{Handler, Neovim};
use nvim_rs::{compat::tokio::Compat, Handler, Neovim};
use tokio; use tokio;
use super::*; use super::*;
use crate::bridge::create_nvim_command; use crate::bridge::{create, create_nvim_command};
#[derive(Clone)] #[derive(Clone)]
pub struct NeovimHandler(); pub struct NeovimHandler();
#[async_trait] #[async_trait]
impl Handler for NeovimHandler { impl Handler for NeovimHandler {
type Writer = Compat<ChildStdin>; type Writer = TxWrapper;
async fn handle_notify( async fn handle_notify(
&self, &self,
_event_name: String, _event_name: String,
_arguments: Vec<Value>, _arguments: Vec<Value>,
_neovim: Neovim<Compat<ChildStdin>>, _neovim: Neovim<TxWrapper>,
) { ) {
} }
} }
@ -288,7 +287,7 @@ mod tests {
let v4: String = format!("neovide_{}", v1); let v4: String = format!("neovide_{}", v1);
let v5: String = format!("neovide_{}", v2); let v5: String = format!("neovide_{}", v2);
let (nvim, _, _) = create::new_child_cmd(&mut create_nvim_command(), NeovimHandler()) let (nvim, _) = create::new_child_cmd(&mut create_nvim_command(), NeovimHandler())
.await .await
.unwrap_or_explained_panic("Could not locate or start the neovim process"); .unwrap_or_explained_panic("Could not locate or start the neovim process");
nvim.set_var(&v4, Value::from(v2.clone())).await.ok(); nvim.set_var(&v4, Value::from(v2.clone())).await.ok();

Loading…
Cancel
Save