Add lua lib & support compiling distant cli on windows (#59)

* Update distant-ssh2 with new changes to wezterm-ssh
* Implement lua module (distant-lua)
* Implement tests for lua module (distant-lua-tests)
* Add untested windows daemon support
* distant binary now compiles on windows
* Split up Github actions for Windows, MacOS, and Linux into individual yaml files
pull/96/head
Chip Senkbeil 3 years ago committed by GitHub
parent b27f0a4109
commit 16bed4690b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,55 @@
name: CI (All)
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
clippy:
name: Lint with clippy
runs-on: ubuntu-latest
env:
RUSTFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v2
- name: Install Rust (clippy)
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: clippy
- uses: Swatinem/rust-cache@v1
- name: Check Cargo availability
run: cargo --version
- name: distant-core (all features)
run: cargo clippy -p distant-core --all-targets --verbose --all-features
- name: distant-ssh2 (all features)
run: cargo clippy -p distant-ssh2 --all-targets --verbose --all-features
- name: distant-lua (lua51 & vendored)
run: (cd distant-lua && cargo clippy --all-targets --verbose --no-default-features --features "lua51,vendored")
shell: bash
- name: distant-lua-tests (lua51 & vendored)
run: (cd distant-lua-tests && cargo clippy --tests --verbose --no-default-features --features "lua51,vendored")
shell: bash
- name: distant (all features)
run: cargo clippy --all-targets --verbose --all-features
rustfmt:
name: Verify code formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust (rustfmt)
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: rustfmt
- uses: Swatinem/rust-cache@v1
- name: Check Cargo availability
run: cargo --version
- run: cargo fmt --all -- --check

@ -1,4 +1,4 @@
name: CI
name: CI (Linux)
on:
push:
@ -17,8 +17,6 @@ jobs:
matrix:
include:
- { rust: stable, os: ubuntu-latest }
- { rust: stable, os: macos-latest }
- { rust: stable, os: windows-latest }
- { rust: 1.51.0, os: ubuntu-latest }
steps:
- uses: actions/checkout@v2
@ -30,8 +28,6 @@ jobs:
- uses: Swatinem/rust-cache@v1
- name: Check Cargo availability
run: cargo --version
- uses: Vampire/setup-wsl@v1
if: ${{ matrix.os == 'windows-latest' }}
- uses: dorny/paths-filter@v2
id: changes
with:
@ -44,6 +40,9 @@ jobs:
- 'distant-core/**'
ssh2:
- 'distant-ssh2/**'
lua:
- 'distant-lua/**'
- 'distant-lua-tests/**'
- name: Run core tests (default features)
run: cargo test --verbose -p distant-core
if: steps.changes.outputs.core == 'true'
@ -52,64 +51,22 @@ jobs:
if: steps.changes.outputs.core == 'true'
- name: Ensure /run/sshd exists on Unix
run: mkdir -p /run/sshd
if: |
matrix.os != 'windows-latest' &&
matrix.os != 'macos-latest' &&
steps.changes.outputs.ssh2 == 'true'
if: steps.changes.outputs.ssh2 == 'true'
- name: Run ssh2 tests (default features)
run: cargo test --verbose -p distant-ssh2
if: |
matrix.os != 'windows-latest' &&
steps.changes.outputs.ssh2 == 'true'
if: steps.changes.outputs.ssh2 == 'true'
- name: Run ssh2 tests (all features)
run: cargo test --verbose --all-features -p distant-ssh2
if: |
matrix.os != 'windows-latest' &&
steps.changes.outputs.ssh2 == 'true'
if: steps.changes.outputs.ssh2 == 'true'
- name: Run CLI tests
run: cargo test --verbose
shell: bash
if: |
matrix.os != 'windows-latest' &&
steps.changes.outputs.cli == 'true'
if: steps.changes.outputs.cli == 'true'
- name: Run CLI tests (no default features)
run: cargo test --verbose --no-default-features
shell: bash
if: |
matrix.os != 'windows-latest' &&
steps.changes.outputs.cli == 'true'
clippy:
name: Lint with clippy
runs-on: ubuntu-latest
env:
RUSTFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v2
- name: Install Rust (clippy)
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: clippy
- uses: Swatinem/rust-cache@v1
- name: Check Cargo availability
run: cargo --version
- run: cargo clippy --workspace --all-targets --verbose
- run: cargo clippy --workspace --all-targets --verbose --all-features
rustfmt:
name: Verify code formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust (rustfmt)
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: rustfmt
- uses: Swatinem/rust-cache@v1
- name: Check Cargo availability
run: cargo --version
- run: cargo fmt --all -- --check
if: steps.changes.outputs.cli == 'true'
- name: Run Lua tests
run: (cd distant-lua && cargo build) && (cd distant-lua-tests && cargo test --verbose)
shell: bash
if: steps.changes.outputs.lua == 'true'

@ -0,0 +1,68 @@
name: CI (MacOS)
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
tests:
name: "Test Rust ${{ matrix.rust }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { rust: stable, os: macos-latest }
steps:
- uses: actions/checkout@v2
- name: Install Rust ${{ matrix.rust }}
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
- uses: Swatinem/rust-cache@v1
- name: Check Cargo availability
run: cargo --version
- uses: dorny/paths-filter@v2
id: changes
with:
base: ${{ github.ref }}
filters: |
cli:
- 'src/**'
- 'Cargo.*'
core:
- 'distant-core/**'
ssh2:
- 'distant-ssh2/**'
lua:
- 'distant-lua/**'
- 'distant-lua-tests/**'
- name: Run core tests (default features)
run: cargo test --verbose -p distant-core
if: steps.changes.outputs.core == 'true'
- name: Run core tests (all features)
run: cargo test --verbose --all-features -p distant-core
if: steps.changes.outputs.core == 'true'
- name: Run ssh2 tests (default features)
run: cargo test --verbose -p distant-ssh2
if: steps.changes.outputs.ssh2 == 'true'
- name: Run ssh2 tests (all features)
run: cargo test --verbose --all-features -p distant-ssh2
if: steps.changes.outputs.ssh2 == 'true'
- name: Run CLI tests
run: cargo test --verbose
shell: bash
if: steps.changes.outputs.cli == 'true'
- name: Run CLI tests (no default features)
run: cargo test --verbose --no-default-features
shell: bash
if: steps.changes.outputs.cli == 'true'
- name: Run Lua tests
run: (cd distant-lua && cargo build) && (cd distant-lua-tests && cargo test --verbose)
shell: bash
if: steps.changes.outputs.lua == 'true'

@ -0,0 +1,80 @@
name: CI (Windows)
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
tests:
name: "Test Rust ${{ matrix.rust }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { rust: stable, os: windows-latest, target: x86_64-pc-windows-msvc }
steps:
- uses: actions/checkout@v2
- name: Install Rust ${{ matrix.rust }}
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v1
- name: Check Cargo availability
run: cargo --version
- uses: Vampire/setup-wsl@v1
- uses: dorny/paths-filter@v2
id: changes
with:
base: ${{ github.ref }}
filters: |
cli:
- 'src/**'
- 'Cargo.*'
core:
- 'distant-core/**'
ssh2:
- 'distant-ssh2/**'
lua:
- 'distant-lua/**'
- 'distant-lua-tests/**'
- name: Run distant-core tests (default features)
run: cargo test --verbose -p distant-core
if: steps.changes.outputs.core == 'true'
- name: Run distant-core tests (all features)
run: cargo test --verbose --all-features -p distant-core
if: steps.changes.outputs.core == 'true'
- name: Build distant-ssh2 (default features)
run: cargo build --verbose -p distant-ssh2
if: steps.changes.outputs.ssh2 == 'true'
- name: Build distant-ssh2 (all features)
run: cargo build --verbose --all-features -p distant-ssh2
if: steps.changes.outputs.ssh2 == 'true'
- name: Build CLI
run: cargo build --verbose
shell: bash
if: steps.changes.outputs.cli == 'true'
- name: Build CLI (no default features)
run: cargo build --verbose --no-default-features
shell: bash
if: steps.changes.outputs.cli == 'true'
- uses: xpol/setup-lua@v0.3
with:
lua-version: "5.1.5"
if: steps.changes.outputs.lua == 'true'
- name: Build Lua (Lua 5.1)
run: |
cd ${{ github.workspace }}\distant-lua
cargo build --verbose --no-default-features --features lua51
shell: cmd
env:
LUA_INC: ${{ github.workspace }}\.lua\include
LUA_LIB: ${{ github.workspace }}\.lua\lib
LUA_LIB_NAME: lua
if: steps.changes.outputs.lua == 'true'

146
Cargo.lock generated

@ -186,7 +186,7 @@ checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0"
[[package]]
name = "async_ossl"
version = "0.1.0"
source = "git+https://github.com/chipsenkbeil/wezterm#5783332027c640d23001f3dad1ece433c424bb36"
source = "git+https://github.com/chipsenkbeil/wezterm#f25fbb737563666006c166233cab10daf853a3b1"
dependencies = [
"openssl",
]
@ -280,6 +280,12 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba"
[[package]]
name = "camino"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d74260d9bf6944e2208aa46841b4b8f0d7ffc0849a06837b2f510337f86b2b"
[[package]]
name = "cc"
version = "1.0.70"
@ -476,6 +482,40 @@ dependencies = [
"walkdir",
]
[[package]]
name = "distant-lua"
version = "0.15.0-alpha.6"
dependencies = [
"distant-core",
"distant-ssh2",
"futures",
"log",
"mlua",
"once_cell",
"oorandom",
"paste",
"rstest",
"serde",
"simplelog",
"tokio",
"whoami",
]
[[package]]
name = "distant-lua-tests"
version = "0.0.0"
dependencies = [
"assert_fs",
"distant-core",
"futures",
"indoc",
"mlua",
"once_cell",
"predicates",
"rstest",
"tokio",
]
[[package]]
name = "distant-ssh2"
version = "0.15.0-alpha.6"
@ -494,6 +534,7 @@ dependencies = [
"rpassword",
"rstest",
"serde",
"shell-words",
"smol",
"tokio",
"wezterm-ssh",
@ -512,6 +553,15 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "erased-serde"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3de9ad4541d99dc22b59134e7ff8dc3d6c988c89ecd7324bf10a8362b07a2afa"
dependencies = [
"serde",
]
[[package]]
name = "event-listener"
version = "2.5.1"
@ -530,7 +580,7 @@ dependencies = [
[[package]]
name = "filedescriptor"
version = "0.8.1"
source = "git+https://github.com/chipsenkbeil/wezterm#5783332027c640d23001f3dad1ece433c424bb36"
source = "git+https://github.com/chipsenkbeil/wezterm#f25fbb737563666006c166233cab10daf853a3b1"
dependencies = [
"libc",
"thiserror",
@ -935,6 +985,24 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "lua-src"
version = "543.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72914332bf1ef0e1185b229135d639f11a4a8ccfd32852db8e52419c04c0247"
dependencies = [
"cc",
]
[[package]]
name = "luajit-src"
version = "210.2.0+resty5f13855"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f85722ea9e022305a077b916c9271011a195ee8dc9b2b764fc78b0378e3b72"
dependencies = [
"cc",
]
[[package]]
name = "memchr"
version = "2.4.1"
@ -963,6 +1031,42 @@ dependencies = [
"winapi",
]
[[package]]
name = "mlua"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10220b40602740bbb1bfa676bb477c7587ec1226d26f9a5f379192ea0a3e24f"
dependencies = [
"bstr 0.2.16",
"cc",
"erased-serde",
"futures-core",
"futures-task",
"futures-util",
"lua-src",
"luajit-src",
"mlua_derive",
"num-traits",
"once_cell",
"pkg-config",
"serde",
]
[[package]]
name = "mlua_derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1713774a29db53a48932596dc943439dd54eb56a9efaace716719cc10fa82d5b"
dependencies = [
"itertools",
"once_cell",
"proc-macro-error",
"proc-macro2",
"quote",
"regex",
"syn",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
@ -1013,6 +1117,12 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "oorandom"
version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "opaque-debug"
version = "0.3.0"
@ -1087,6 +1197,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "paste"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
[[package]]
name = "pin-project-lite"
version = "0.2.7"
@ -1132,7 +1248,7 @@ dependencies = [
[[package]]
name = "portable-pty"
version = "0.5.0"
source = "git+https://github.com/chipsenkbeil/wezterm#5783332027c640d23001f3dad1ece433c424bb36"
source = "git+https://github.com/chipsenkbeil/wezterm#f25fbb737563666006c166233cab10daf853a3b1"
dependencies = [
"anyhow",
"bitflags",
@ -1510,6 +1626,17 @@ dependencies = [
"libc",
]
[[package]]
name = "simplelog"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85d04ae642154220ef00ee82c36fb07853c10a4f2a0ca6719f9991211d2eb959"
dependencies = [
"chrono",
"log",
"termcolor",
]
[[package]]
name = "slab"
version = "0.4.4"
@ -1643,6 +1770,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "termios"
version = "0.2.2"
@ -1917,11 +2053,13 @@ dependencies = [
[[package]]
name = "wezterm-ssh"
version = "0.2.0"
source = "git+https://github.com/chipsenkbeil/wezterm#5783332027c640d23001f3dad1ece433c424bb36"
source = "git+https://github.com/chipsenkbeil/wezterm#f25fbb737563666006c166233cab10daf853a3b1"
dependencies = [
"anyhow",
"async_ossl",
"base64",
"bitflags",
"camino",
"dirs-next",
"filedescriptor",
"filenamegen",

@ -12,7 +12,7 @@ readme = "README.md"
license = "MIT OR Apache-2.0"
[workspace]
members = ["distant-core", "distant-ssh2"]
members = ["distant-core", "distant-lua", "distant-lua-tests", "distant-ssh2"]
[profile.release]
opt-level = 'z'
@ -30,7 +30,6 @@ ssh2 = ["distant-ssh2"]
derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error", "is_variant"] }
distant-core = { version = "=0.15.0-alpha.6", path = "distant-core", features = ["structopt"] }
flexi_logger = "0.18.0"
fork = "0.1.18"
log = "0.4.14"
once_cell = "1.8.0"
rand = { version = "0.8.4", features = ["getrandom"] }
@ -43,6 +42,9 @@ whoami = "1.1.2"
# Optional native SSH functionality
distant-ssh2 = { version = "=0.15.0-alpha.6", path = "distant-ssh2", optional = true }
[target.'cfg(unix)'.dependencies]
fork = "0.1.18"
[dev-dependencies]
assert_cmd = "2.0.0"
assert_fs = "1.0.4"

@ -2,6 +2,7 @@
name = "distant-core"
description = "Core library for distant, enabling operation on a remote computer through file and process manipulation"
categories = ["network-programming"]
keywords = ["api", "async"]
version = "0.15.0-alpha.6"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"

@ -6,7 +6,10 @@ use std::{
io::{self, Cursor, Read},
ops::{Deref, DerefMut},
};
use tokio::{sync::mpsc, task::JoinHandle};
use tokio::{
sync::mpsc::{self, error::TryRecvError},
task::JoinHandle,
};
mod data;
pub use data::*;
@ -74,18 +77,24 @@ impl RemoteLspStdin {
Self { inner, buf: None }
}
/// Writes data to the stdin of a specific remote process
pub async fn write(&mut self, data: &str) -> io::Result<()> {
// Create or insert into our buffer
match &mut self.buf {
Some(buf) => buf.push_str(data),
None => self.buf = Some(data.to_string()),
/// Tries to write data to the stdin of a specific remote process
pub fn try_write(&mut self, data: &str) -> io::Result<()> {
let queue = self.update_and_read_messages(data)?;
// Process and then send out each LSP message in our queue
for mut data in queue {
// Convert distant:// to file://
data.mut_content().convert_distant_scheme_to_local();
data.refresh_content_length();
self.inner.try_write(&data.to_string())?;
}
// Read LSP messages from our internal buffer
let buf = self.buf.take().unwrap();
let (remainder, queue) = read_lsp_messages(buf)?;
self.buf = remainder;
Ok(())
}
/// Writes data to the stdin of a specific remote process
pub async fn write(&mut self, data: &str) -> io::Result<()> {
let queue = self.update_and_read_messages(data)?;
// Process and then send out each LSP message in our queue
for mut data in queue {
@ -97,6 +106,30 @@ impl RemoteLspStdin {
Ok(())
}
fn update_and_read_messages(&mut self, data: &str) -> io::Result<Vec<LspData>> {
// Create or insert into our buffer
match &mut self.buf {
Some(buf) => buf.push_str(data),
None => self.buf = Some(data.to_string()),
}
// Read LSP messages from our internal buffer
let buf = self.buf.take().unwrap();
match read_lsp_messages(&buf) {
// If we succeed, update buf with our remainder and return messages
Ok((remainder, queue)) => {
self.buf = remainder;
Ok(queue)
}
// Otherwise, if failed, reset buf back to what it was
Err(x) => {
self.buf = Some(buf);
Err(x)
}
}
}
}
/// A handle to a remote LSP process' standard output (stdout)
@ -121,6 +154,18 @@ impl RemoteLspStdout {
Self { read_task, rx }
}
/// Tries to read a complete LSP message over stdout, returning `None` if no complete message
/// is available
pub fn try_read(&mut self) -> io::Result<Option<String>> {
match self.rx.try_recv() {
Ok(Ok(data)) => Ok(Some(data)),
Ok(Err(x)) => Err(x),
Err(TryRecvError::Empty) => Ok(None),
Err(TryRecvError::Disconnected) => Err(io::Error::from(io::ErrorKind::BrokenPipe)),
}
}
/// Reads a complete LSP message over stdout
pub async fn read(&mut self) -> io::Result<String> {
self.rx
.recv()
@ -158,6 +203,18 @@ impl RemoteLspStderr {
Self { read_task, rx }
}
/// Tries to read a complete LSP message over stderr, returning `None` if no complete message
/// is available
pub fn try_read(&mut self) -> io::Result<Option<String>> {
match self.rx.try_recv() {
Ok(Ok(data)) => Ok(Some(data)),
Ok(Err(x)) => Err(x),
Err(TryRecvError::Empty) => Ok(None),
Err(TryRecvError::Disconnected) => Err(io::Error::from(io::ErrorKind::BrokenPipe)),
}
}
/// Reads a complete LSP message over stderr
pub async fn read(&mut self) -> io::Result<String> {
self.rx
.recv()
@ -190,7 +247,7 @@ where
// Read LSP messages from our internal buffer
let buf = task_buf.take().unwrap();
let (remainder, queue) = match read_lsp_messages(buf) {
let (remainder, queue) = match read_lsp_messages(&buf) {
Ok(x) => x,
Err(x) => {
let _ = tx.send(Err(x)).await;
@ -218,7 +275,7 @@ where
(read_task, rx)
}
fn read_lsp_messages(input: String) -> io::Result<(Option<String>, Vec<LspData>)> {
fn read_lsp_messages(input: &str) -> io::Result<(Option<String>, Vec<LspData>)> {
let mut queue = Vec::new();
// Continue to read complete messages from the input until we either fail to parse or we reach

@ -1,6 +1,6 @@
use crate::{
client::{Mailbox, SessionChannel},
constants::CLIENT_MAILBOX_CAPACITY,
constants::CLIENT_PIPE_CAPACITY,
data::{Request, RequestData, ResponseData},
net::TransportError,
};
@ -8,7 +8,10 @@ use derive_more::{Display, Error, From};
use log::*;
use tokio::{
io,
sync::mpsc,
sync::mpsc::{
self,
error::{TryRecvError, TrySendError},
},
task::{JoinError, JoinHandle},
};
@ -105,9 +108,9 @@ impl RemoteProcess {
};
// Create channels for our stdin/stdout/stderr
let (stdin_tx, stdin_rx) = mpsc::channel(CLIENT_MAILBOX_CAPACITY);
let (stdout_tx, stdout_rx) = mpsc::channel(CLIENT_MAILBOX_CAPACITY);
let (stderr_tx, stderr_rx) = mpsc::channel(CLIENT_MAILBOX_CAPACITY);
let (stdin_tx, stdin_rx) = mpsc::channel(CLIENT_PIPE_CAPACITY);
let (stdout_tx, stdout_rx) = mpsc::channel(CLIENT_PIPE_CAPACITY);
let (stderr_tx, stderr_rx) = mpsc::channel(CLIENT_PIPE_CAPACITY);
// Used to terminate request task, either explicitly by the process or internally
// by the response task when it terminates
@ -173,6 +176,15 @@ impl RemoteProcess {
pub struct RemoteStdin(mpsc::Sender<String>);
impl RemoteStdin {
/// Tries to write to the stdin of the remote process
pub fn try_write(&mut self, data: impl Into<String>) -> io::Result<()> {
match self.0.try_send(data.into()) {
Ok(data) => Ok(data),
Err(TrySendError::Full(_)) => Err(io::Error::from(io::ErrorKind::WouldBlock)),
Err(TrySendError::Closed(_)) => Err(io::Error::from(io::ErrorKind::BrokenPipe)),
}
}
/// Writes data to the stdin of a specific remote process
pub async fn write(&mut self, data: impl Into<String>) -> io::Result<()> {
self.0
@ -180,6 +192,11 @@ impl RemoteStdin {
.await
.map_err(|x| io::Error::new(io::ErrorKind::BrokenPipe, x))
}
/// Checks if stdin has been closed
pub fn is_closed(&self) -> bool {
self.0.is_closed()
}
}
/// A handle to a remote process' standard output (stdout)
@ -187,6 +204,16 @@ impl RemoteStdin {
pub struct RemoteStdout(mpsc::Receiver<String>);
impl RemoteStdout {
/// Tries to receive latest stdout for a remote process, yielding `None`
/// if no stdout is available
pub fn try_read(&mut self) -> io::Result<Option<String>> {
match self.0.try_recv() {
Ok(data) => Ok(Some(data)),
Err(TryRecvError::Empty) => Ok(None),
Err(TryRecvError::Disconnected) => Err(io::Error::from(io::ErrorKind::BrokenPipe)),
}
}
/// Retrieves the latest stdout for a specific remote process
pub async fn read(&mut self) -> io::Result<String> {
self.0
@ -201,6 +228,16 @@ impl RemoteStdout {
pub struct RemoteStderr(mpsc::Receiver<String>);
impl RemoteStderr {
/// Tries to receive latest stderr for a remote process, yielding `None`
/// if no stderr is available
pub fn try_read(&mut self) -> io::Result<Option<String>> {
match self.0.try_recv() {
Ok(data) => Ok(Some(data)),
Err(TryRecvError::Empty) => Ok(None),
Err(TryRecvError::Disconnected) => Err(io::Error::from(io::ErrorKind::BrokenPipe)),
}
}
/// Retrieves the latest stderr for a specific remote process
pub async fn read(&mut self) -> io::Result<String> {
self.0

@ -154,10 +154,10 @@ pub trait SessionChannelExt {
macro_rules! make_body {
($self:expr, $tenant:expr, $data:expr, @ok) => {
make_body!($self, $tenant, $data, |data| {
if data.is_ok() {
Ok(())
} else {
Err(SessionChannelExtError::MismatchedResponse)
match data {
ResponseData::Ok => Ok(()),
ResponseData::Error(x) => Err(SessionChannelExtError::Failure(x)),
_ => Err(SessionChannelExtError::MismatchedResponse),
}
})
};

@ -1,6 +1,9 @@
/// Capacity associated with a client mailboxes for receiving multiple responses to a request
pub const CLIENT_MAILBOX_CAPACITY: usize = 10000;
/// Capacity associated stdin, stdout, and stderr pipes receiving data from remote server
pub const CLIENT_PIPE_CAPACITY: usize = 10000;
/// Represents the maximum size (in bytes) that data will be read from pipes
/// per individual `read` call
///

@ -4,7 +4,7 @@ mod transport;
use derive_more::{Display, Error};
pub use listener::{AcceptFuture, Listener, TransportListener};
use rand::{rngs::OsRng, RngCore};
use std::fmt;
use std::{fmt, str::FromStr};
pub use transport::*;
#[derive(Debug, Display, Error)]
@ -81,6 +81,15 @@ impl<const N: usize> SecretKey<N> {
}
}
impl<const N: usize> FromStr for SecretKey<N> {
type Err = SecretKeyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = hex::decode(s).map_err(|_| SecretKeyError)?;
Self::from_slice(&bytes)
}
}
pub trait UnprotectedToHexKey {
fn unprotected_to_hex_key(&self) -> String;
}

@ -0,0 +1,8 @@
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-args=-rdynamic"]
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-args=-rdynamic"]
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-args=-rdynamic"]

@ -0,0 +1,27 @@
[package]
name = "distant-lua-tests"
description = "Tests for distant-lua crate"
version = "0.0.0"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"
publish = false
[features]
default = ["lua51", "vendored"]
lua54 = ["mlua/lua54"]
lua53 = ["mlua/lua53"]
lua52 = ["mlua/lua52"]
lua51 = ["mlua/lua51"]
luajit = ["mlua/luajit"]
vendored = ["mlua/vendored"]
[dependencies]
assert_fs = "1.0.4"
distant-core = { path = "../distant-core" }
futures = "0.3.17"
indoc = "1.0.3"
mlua = { version = "0.6.5", features = ["async", "macros", "serialize"] }
once_cell = "1.8.0"
predicates = "2.0.2"
rstest = "0.11.0"
tokio = { version = "1.12.0", features = ["rt", "sync"] }

@ -0,0 +1,34 @@
# Tests for Distant Lua (module)
Contains tests for the **distant-lua** module. These tests must be in a
separate crate due to linking restrictions as described in
[khvzak/mlua#79](https://github.com/khvzak/mlua/issues/79).
## Tests
You must run these tests from within this directory, not from the root of the
repository. Additionally, you must build the Lua module **before** running
these tests!
```bash
# From root of repository
(cd distant-lua && cargo build --release)
```
Running the tests themselves:
```bash
# From root of repository
(cd distant-lua-tests && cargo test --release)
```
## License
This project is licensed under either of
Apache License, Version 2.0, (LICENSE-APACHE or
[apache-license][apache-license]) MIT license (LICENSE-MIT or
[mit-license][mit-license]) at your option.
[apache-license]: http://www.apache.org/licenses/LICENSE-2.0
[mit-license]: http://opensource.org/licenses/MIT

@ -0,0 +1,74 @@
use distant_core::*;
use once_cell::sync::OnceCell;
use rstest::*;
use std::{net::SocketAddr, thread};
use tokio::{runtime::Runtime, sync::mpsc};
/// Context for some listening distant server
pub struct DistantServerCtx {
pub addr: SocketAddr,
pub key: String,
done_tx: mpsc::Sender<()>,
}
impl DistantServerCtx {
pub fn initialize() -> Self {
let ip_addr = "127.0.0.1".parse().unwrap();
let (done_tx, mut done_rx) = mpsc::channel(1);
let (started_tx, mut started_rx) = mpsc::channel(1);
// NOTE: We spawn a dedicated thread that runs our tokio runtime separately from our test
// itself because using lua blocks the thread and prevents our runtime from working unless
// we make the tokio test multi-threaded using `tokio::test(flavor = "multi_thread",
// worker_threads = 1)` which isn't great because we're only using async tests for our
// server itself; so, we hide that away since our test logic doesn't need to be async
thread::spawn(move || match Runtime::new() {
Ok(rt) => {
rt.block_on(async move {
let opts = DistantServerOptions {
shutdown_after: None,
max_msg_capacity: 100,
};
let key = SecretKey::default();
let key_hex_string = key.unprotected_to_hex_key();
let codec = XChaCha20Poly1305Codec::from(key);
let (_server, port) =
DistantServer::bind(ip_addr, "0".parse().unwrap(), codec, opts)
.await
.unwrap();
started_tx.send(Ok((port, key_hex_string))).await.unwrap();
let _ = done_rx.recv().await;
});
}
Err(x) => {
started_tx.blocking_send(Err(x)).unwrap();
}
});
// Extract our server startup data if we succeeded
let (port, key) = started_rx.blocking_recv().unwrap().unwrap();
Self {
addr: SocketAddr::new(ip_addr, port),
key,
done_tx,
}
}
}
impl Drop for DistantServerCtx {
/// Kills server upon drop
fn drop(&mut self) {
let _ = self.done_tx.send(());
}
}
/// Returns a reference to the global distant server
#[fixture]
pub fn ctx() -> &'static DistantServerCtx {
static CTX: OnceCell<DistantServerCtx> = OnceCell::new();
CTX.get_or_init(DistantServerCtx::initialize)
}

@ -0,0 +1,41 @@
use mlua::prelude::*;
use std::{env, path::PathBuf};
pub fn make() -> LuaResult<Lua> {
let (dylib_path, dylib_ext, separator);
if cfg!(target_os = "macos") {
dylib_path = env::var("DYLD_FALLBACK_LIBRARY_PATH").unwrap();
dylib_ext = "dylib";
separator = ":";
} else if cfg!(target_os = "linux") {
dylib_path = env::var("LD_LIBRARY_PATH").unwrap();
dylib_ext = "so";
separator = ":";
} else if cfg!(target_os = "windows") {
dylib_path = env::var("PATH").unwrap();
dylib_ext = "dll";
separator = ";";
} else {
panic!("unknown target os");
};
let mut cpath = dylib_path
.split(separator)
.take(3)
.map(|p| {
let mut path = PathBuf::from(p);
path.push(format!("lib?.{}", dylib_ext));
path.to_str().unwrap().to_owned()
})
.collect::<Vec<_>>()
.join(";");
if cfg!(target_os = "windows") {
cpath = cpath.replace("\\", "\\\\");
cpath = cpath.replace("lib?.", "?.");
}
let lua = unsafe { Lua::unsafe_new() }; // To be able to load C modules
lua.load(&format!("package.cpath = \"{}\"", cpath)).exec()?;
Ok(lua)
}

@ -0,0 +1,4 @@
pub mod fixtures;
pub mod lua;
pub mod poll;
pub mod session;

@ -0,0 +1,17 @@
use mlua::{chunk, prelude::*};
use std::{thread, time::Duration};
/// Creates a function that can be passed as the schedule function for `wrap_async`
pub fn make_function(lua: &Lua) -> LuaResult<LuaFunction> {
let sleep = lua.create_function(|_, ()| {
thread::sleep(Duration::from_millis(10));
Ok(())
})?;
lua.load(chunk! {
local cb = ...
$sleep()
cb()
})
.into_function()
}

@ -0,0 +1,41 @@
use super::fixtures::DistantServerCtx;
use mlua::{chunk, prelude::*};
/// Creates a function that produces a session within the provided Lua environment
/// using the given distant server context, returning the session's id
pub fn make_function<'a>(lua: &'a Lua, ctx: &'_ DistantServerCtx) -> LuaResult<LuaFunction<'a>> {
let addr = ctx.addr;
let host = addr.ip().to_string();
let port = addr.port();
let key = ctx.key.clone();
lua.load(chunk! {
local distant = require("distant_lua")
local thread = coroutine.create(distant.session.connect_async)
local status, res = coroutine.resume(thread, {
host = $host,
port = $port,
key = $key,
timeout = 15000,
})
// Block until the connection finishes
local session = nil
while status do
if status and res ~= distant.PENDING then
session = res
break
end
status, res = coroutine.resume(thread)
end
if session then
return session
else
error(res)
end
})
.into_function()
}

@ -0,0 +1,2 @@
mod common;
mod lua;

@ -0,0 +1,79 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_yield_error_if_fails_to_create_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Create a temporary path and add to it to ensure that there are
// extra components that don't exist to cause writing to fail
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("dir").child("test-file");
let file_path = file.path().to_str().unwrap();
let data = b"some text".to_vec();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.append_file_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path, data = $data }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we didn't actually create the file
file.assert(predicate::path::missing());
}
#[rstest]
fn should_append_data_to_existing_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("line 1").unwrap();
let file_path = file.path().to_str().unwrap();
let data = b"some text".to_vec();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.append_file_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path, data = $data }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we appended to the file
file.assert("line 1some text");
}

@ -0,0 +1,79 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_yield_error_if_fails_to_create_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Create a temporary path and add to it to ensure that there are
// extra components that don't exist to cause writing to fail
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("dir").child("test-file");
let file_path = file.path().to_str().unwrap();
let text = "some text".to_string();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.append_file_text_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path, data = $text }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we didn't actually create the file
file.assert(predicate::path::missing());
}
#[rstest]
fn should_append_data_to_existing_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("line 1").unwrap();
let file_path = file.path().to_str().unwrap();
let text = "some text".to_string();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.append_file_text_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path, data = $text }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we appended to the file
file.assert("line 1some text");
}

@ -0,0 +1,210 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_send_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
let dst = temp.child("dst");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.copy_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { src = $src_path, dst = $dst_path }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that destination does not exist
dst.assert(predicate::path::missing());
}
#[rstest]
fn should_support_copying_an_entire_directory(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.create_dir_all().unwrap();
let src_file = src.child("file");
src_file.write_str("some contents").unwrap();
let dst = temp.child("dst");
let dst_file = dst.child("file");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.copy_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { src = $src_path, dst = $dst_path }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we have source and destination directories and associated contents
src.assert(predicate::path::is_dir());
src_file.assert(predicate::path::is_file());
dst.assert(predicate::path::is_dir());
dst_file.assert(predicate::path::eq_file(src_file.path()));
}
#[rstest]
fn should_support_copying_an_empty_directory(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.create_dir_all().unwrap();
let dst = temp.child("dst");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.copy_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { src = $src_path, dst = $dst_path }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we still have source and destination directories
src.assert(predicate::path::is_dir());
dst.assert(predicate::path::is_dir());
}
#[rstest]
fn should_support_copying_a_directory_that_only_contains_directories(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.create_dir_all().unwrap();
let src_dir = src.child("dir");
src_dir.create_dir_all().unwrap();
let dst = temp.child("dst");
let dst_dir = dst.child("dir");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.copy_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { src = $src_path, dst = $dst_path }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we have source and destination directories and associated contents
src.assert(predicate::path::is_dir().name("src"));
src_dir.assert(predicate::path::is_dir().name("src/dir"));
dst.assert(predicate::path::is_dir().name("dst"));
dst_dir.assert(predicate::path::is_dir().name("dst/dir"));
}
#[rstest]
fn should_support_copying_a_single_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.write_str("some text").unwrap();
let dst = temp.child("dst");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.copy_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { src = $src_path, dst = $dst_path }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we still have source and that destination has source's contents
src.assert(predicate::path::is_file());
dst.assert(predicate::path::eq_file(src.path()));
}

@ -0,0 +1,129 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
// /root/
// /root/file1
// /root/link1 -> /root/sub1/file2
// /root/sub1/
// /root/sub1/file2
fn setup_dir() -> assert_fs::TempDir {
let root_dir = assert_fs::TempDir::new().unwrap();
root_dir.child("file1").touch().unwrap();
let sub1 = root_dir.child("sub1");
sub1.create_dir_all().unwrap();
let file2 = sub1.child("file2");
file2.touch().unwrap();
let link1 = root_dir.child("link1");
link1.symlink_to_file(file2.path()).unwrap();
root_dir
}
#[rstest]
fn should_send_error_if_fails(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Make a path that has multiple non-existent components
// so the creation will fail
let root_dir = setup_dir();
let path = root_dir.path().join("nested").join("new-dir");
let path_str = path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.create_dir_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $path_str }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that the directory was not actually created
assert!(!path.exists(), "Path unexpectedly exists");
}
#[rstest]
fn should_send_ok_when_successful(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let root_dir = setup_dir();
let path = root_dir.path().join("new-dir");
let path_str = path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.create_dir_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $path_str }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that the directory was actually created
assert!(path.exists(), "Directory not created");
}
#[rstest]
fn should_support_creating_multiple_dir_components(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let root_dir = setup_dir();
let path = root_dir.path().join("nested").join("new-dir");
let path_str = path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.create_dir_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $path_str, all = true }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that the directory was actually created
assert!(path.exists(), "Directory not created");
}

@ -0,0 +1,73 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
#[rstest]
fn should_send_true_if_path_exists(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.touch().unwrap();
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.exists_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, exists
f(session, { path = $file_path }, function(success, res)
if success then
exists = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(exists == true, "Invalid exists return value")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_send_false_if_path_does_not_exist(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.exists_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, exists
f(session, { path = $file_path }, function(success, res)
if success then
exists = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(exists == false, "Invalid exists return value")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,238 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
#[rstest]
fn should_send_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.metadata_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_metadata_on_file_if_exists(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.write_str("some text").unwrap();
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.metadata_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, metadata
f(session, { path = $file_path }, function(success, res)
if success then
metadata = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(metadata, "Missing metadata")
assert(not metadata.canonicalized_path, "Unexpectedly got canonicalized path")
assert(metadata.file_type == "file", "Got wrong file type: " .. metadata.file_type)
assert(metadata.len == 9, "Got wrong len: " .. metadata.len)
assert(not metadata.readonly, "Unexpectedly readonly")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_metadata_on_dir_if_exists(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let dir = temp.child("dir");
dir.create_dir_all().unwrap();
let dir_path = dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.metadata_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, metadata
f(session, { path = $dir_path }, function(success, res)
if success then
metadata = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(metadata, "Missing metadata")
assert(not metadata.canonicalized_path, "Unexpectedly got canonicalized path")
assert(metadata.file_type == "dir", "Got wrong file type: " .. metadata.file_type)
assert(not metadata.readonly, "Unexpectedly readonly")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_metadata_on_symlink_if_exists(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.write_str("some text").unwrap();
let symlink = temp.child("link");
symlink.symlink_to_file(file.path()).unwrap();
let symlink_path = symlink.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.metadata_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, metadata
f(session, { path = $symlink_path }, function(success, res)
if success then
metadata = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(metadata, "Missing metadata")
assert(not metadata.canonicalized_path, "Unexpectedly got canonicalized path")
assert(metadata.file_type == "symlink", "Got wrong file type: " .. metadata.file_type)
assert(not metadata.readonly, "Unexpectedly readonly")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_include_canonicalized_path_if_flag_specified(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.write_str("some text").unwrap();
let file_path = file.path().canonicalize().unwrap();
let file_path_str = file_path.to_str().unwrap();
let symlink = temp.child("link");
symlink.symlink_to_file(file.path()).unwrap();
let symlink_path = symlink.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.metadata_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, metadata
f(session, { path = $symlink_path, canonicalize = true }, function(success, res)
if success then
metadata = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(metadata, "Missing metadata")
assert(
metadata.canonicalized_path == $file_path_str,
"Got wrong canonicalized path: " .. metadata.canonicalized_path
)
assert(metadata.file_type == "symlink", "Got wrong file type: " .. metadata.file_type)
assert(not metadata.readonly, "Unexpectedly readonly")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_resolve_file_type_of_symlink_if_flag_specified(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.write_str("some text").unwrap();
let symlink = temp.child("link");
symlink.symlink_to_file(file.path()).unwrap();
let symlink_path = symlink.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.metadata_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, metadata
f(session, { path = $symlink_path, resolve_file_type = true }, function(success, res)
if success then
metadata = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(metadata, "Missing metadata")
assert(metadata.file_type == "file", "Got wrong file type: " .. metadata.file_type)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,14 @@
mod append_file;
mod append_file_text;
mod copy;
mod create_dir;
mod exists;
mod metadata;
mod read_dir;
mod read_file;
mod read_file_text;
mod remove;
mod rename;
mod spawn;
mod write_file;
mod write_file_text;

@ -0,0 +1,357 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
// /root/
// /root/file1
// /root/link1 -> /root/sub1/file2
// /root/sub1/
// /root/sub1/file2
fn setup_dir() -> assert_fs::TempDir {
let root_dir = assert_fs::TempDir::new().unwrap();
root_dir.child("file1").touch().unwrap();
let sub1 = root_dir.child("sub1");
sub1.create_dir_all().unwrap();
let file2 = sub1.child("file2");
file2.touch().unwrap();
let link1 = root_dir.child("link1");
link1.symlink_to_file(file2.path()).unwrap();
root_dir
}
#[rstest]
fn should_return_error_if_directory_does_not_exist(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let dir = temp.child("test-dir");
let dir_path = dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.read_dir_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $dir_path }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_have_depth_default_to_1(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.read_dir_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, tbl
f(session, { path = $root_dir_path }, function(success, res)
if success then
tbl = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(tbl, "Missing result")
local entries = tbl.entries
assert(entries[1].file_type == "file", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == "file1", "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 1, "Wrong depth")
assert(entries[2].file_type == "symlink", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == "link1", "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "dir", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == "sub1", "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_depth_limits(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.read_dir_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, tbl
f(session, { path = $root_dir_path, depth = 1 }, function(success, res)
if success then
tbl = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(tbl, "Missing result")
local entries = tbl.entries
assert(entries[1].file_type == "file", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == "file1", "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 1, "Wrong depth")
assert(entries[2].file_type == "symlink", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == "link1", "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "dir", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == "sub1", "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_unlimited_depth_using_zero(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.read_dir_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, tbl
f(session, { path = $root_dir_path, depth = 0 }, function(success, res)
if success then
tbl = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(tbl, "Missing result")
local entries = tbl.entries
assert(entries[1].file_type == "file", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == "file1", "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 1, "Wrong depth")
assert(entries[2].file_type == "symlink", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == "link1", "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "dir", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == "sub1", "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
assert(entries[4].file_type == "file", "Wrong file type: " .. entries[4].file_type)
assert(entries[4].path == "sub1/file2", "Wrong path: " .. entries[4].path)
assert(entries[4].depth == 2, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_including_directory_in_returned_entries(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let root_dir_canonicalized_path = root_dir.path().canonicalize().unwrap();
let root_dir_canonicalized_path_str = root_dir_canonicalized_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.read_dir_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, tbl
f(session, { path = $root_dir_path, include_root = true }, function(success, res)
if success then
tbl = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(tbl, "Missing result")
local entries = tbl.entries
assert(entries[1].file_type == "dir", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == $root_dir_canonicalized_path_str, "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 0, "Wrong depth")
assert(entries[2].file_type == "file", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == "file1", "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "symlink", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == "link1", "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
assert(entries[4].file_type == "dir", "Wrong file type: " .. entries[4].file_type)
assert(entries[4].path == "sub1", "Wrong path: " .. entries[4].path)
assert(entries[4].depth == 1, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_returning_absolute_paths(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let root_dir_canonicalized_path = root_dir.path().canonicalize().unwrap();
let file1_path = root_dir_canonicalized_path.join("file1");
let link1_path = root_dir_canonicalized_path.join("link1");
let sub1_path = root_dir_canonicalized_path.join("sub1");
let file1_path_str = file1_path.to_str().unwrap();
let link1_path_str = link1_path.to_str().unwrap();
let sub1_path_str = sub1_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.read_dir_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, tbl
f(session, { path = $root_dir_path, absolute = true }, function(success, res)
if success then
tbl = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(tbl, "Missing result")
local entries = tbl.entries
assert(entries[1].file_type == "file", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == $file1_path_str, "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 1, "Wrong depth")
assert(entries[2].file_type == "symlink", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == $link1_path_str, "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "dir", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == $sub1_path_str, "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_returning_canonicalized_paths(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.read_dir_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, tbl
f(session, { path = $root_dir_path, canonicalize = true }, function(success, res)
if success then
tbl = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(tbl, "Missing result")
local entries = tbl.entries
assert(entries[1].file_type == "file", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == "file1", "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 1, "Wrong depth")
assert(entries[2].file_type == "symlink", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == "sub1/file2", "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "dir", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == "sub1", "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,79 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
#[rstest]
fn should_return_error_if_fails_to_read_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("missing-file");
let file_path = file.path();
let file_path_str = file_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.read_file_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path_str }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_file_contents_as_byte_list(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("abcd").unwrap();
let file_path = file.path();
let file_path_str = file_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.read_file_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, contents
f(session, { path = $file_path_str }, function(success, res)
if success then
contents = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(contents, "Missing file contents")
// abcd -> {97, 98, 99, 100}
assert(type(contents) == "table", "Wrong content type: " .. type(contents))
assert(contents[1] == 97, "Unexpected first byte: " .. contents[1])
assert(contents[2] == 98, "Unexpected second byte: " .. contents[2])
assert(contents[3] == 99, "Unexpected third byte: " .. contents[3])
assert(contents[4] == 100, "Unexpected fourth byte: " .. contents[4])
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,74 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
#[rstest]
fn should_return_error_if_fails_to_read_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("missing-file");
let file_path = file.path();
let file_path_str = file_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.read_file_text_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path_str }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_file_contents_as_byte_list(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("some file contents").unwrap();
let file_path = file.path();
let file_path_str = file_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local contents = session:read_file({ path = $file_path_str })
local f = require("distant_lua").utils.wrap_async(
session.read_file_text_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, contents
f(session, { path = $file_path_str }, function(success, res)
if success then
contents = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(contents, "Missing file contents")
assert(contents == "some file contents", "Unexpected file contents: " .. contents)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,145 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_return_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("missing-file");
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.remove_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that path does not exist
file.assert(predicate::path::missing());
}
#[rstest]
fn should_support_deleting_a_directory(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let dir = temp.child("dir");
dir.create_dir_all().unwrap();
let dir_path = dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.remove_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $dir_path }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that path does not exist
dir.assert(predicate::path::missing());
}
#[rstest]
fn should_delete_nonempty_directory_if_force_is_true(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let dir = temp.child("dir");
dir.create_dir_all().unwrap();
dir.child("file").touch().unwrap();
let dir_path = dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.remove_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $dir_path, force = true }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that path does not exist
dir.assert(predicate::path::missing());
}
#[rstest]
fn should_support_deleting_a_single_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("some-file");
file.touch().unwrap();
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.remove_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that path does not exist
file.assert(predicate::path::missing());
}

@ -0,0 +1,126 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_return_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
let dst = temp.child("dst");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.rename_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { src = $src_path, dst = $dst_path }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that destination does not exist
dst.assert(predicate::path::missing());
}
#[rstest]
fn should_support_renaming_an_entire_directory(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.create_dir_all().unwrap();
let src_file = src.child("file");
src_file.write_str("some contents").unwrap();
let dst = temp.child("dst");
let dst_file = dst.child("file");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.rename_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { src = $src_path, dst = $dst_path }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we moved the contents
src.assert(predicate::path::missing());
src_file.assert(predicate::path::missing());
dst.assert(predicate::path::is_dir());
dst_file.assert("some contents");
}
#[rstest]
fn should_support_renaming_a_single_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.write_str("some text").unwrap();
let dst = temp.child("dst");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.rename_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { src = $src_path, dst = $dst_path }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we moved the file
src.assert(predicate::path::missing());
dst.assert("some text");
}

@ -0,0 +1,437 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use once_cell::sync::Lazy;
use rstest::*;
static TEMP_SCRIPT_DIR: Lazy<assert_fs::TempDir> = Lazy::new(|| assert_fs::TempDir::new().unwrap());
static SCRIPT_RUNNER: Lazy<String> = Lazy::new(|| String::from("bash"));
static ECHO_ARGS_TO_STDOUT_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stdout.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
printf "%s" "$*"
"#
))
.unwrap();
script
});
static ECHO_ARGS_TO_STDERR_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stderr.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
printf "%s" "$*" 1>&2
"#
))
.unwrap();
script
});
static ECHO_STDIN_TO_STDOUT_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_stdin_to_stdout.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
while IFS= read; do echo "$REPLY"; done
"#
))
.unwrap();
script
});
static SLEEP_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("sleep.sh");
script
.write_str(indoc::indoc!(
r#"
#!/usr/bin/env bash
sleep "$1"
"#
))
.unwrap();
script
});
static DOES_NOT_EXIST_BIN: Lazy<assert_fs::fixture::ChildPath> =
Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin"));
#[rstest]
fn should_return_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = DOES_NOT_EXIST_BIN.to_str().unwrap().to_string();
let args: Vec<String> = Vec::new();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { cmd = $cmd, args = $args }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_back_process_on_success(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string()];
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process")
assert(proc.id >= 0, "Invalid process returned")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_return_process_that_can_retrieve_stdout(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![
ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string(),
String::from("some stdout"),
];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process")
assert(proc, "Missing proc")
// Wait briefly to ensure the process sends stdout
$wait_fn()
local f = distant.utils.wrap_async(proc.read_stdout_async, $schedule_fn)
local err, stdout
f(proc, function(success, res)
if success then
stdout = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed reading stdout")
assert(stdout == "some stdout", "Unexpected stdout: " .. stdout)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_return_process_that_can_retrieve_stderr(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![
ECHO_ARGS_TO_STDERR_SH.to_str().unwrap().to_string(),
String::from("some stderr"),
];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process")
assert(proc, "Missing proc")
// Wait briefly to ensure the process sends stdout
$wait_fn()
local f = distant.utils.wrap_async(proc.read_stderr_async, $schedule_fn)
local err, stderr
f(proc, function(success, res)
if success then
stderr = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed reading stdout")
assert(stderr == "some stderr", "Unexpected stderr: " .. stderr)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_error_when_killing_dead_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Spawn a process that will exit immediately, but is a valid process
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("0")];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process")
assert(proc, "Missing proc")
// Wait briefly to ensure the process dies
$wait_fn()
local f = distant.utils.wrap_async(proc.kill_async, $schedule_fn)
local err
f(proc, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded in killing dead process")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_killing_processing(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("1")];
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process")
assert(proc, "Missing proc")
local f = distant.utils.wrap_async(proc.kill_async, $schedule_fn)
local err
f(proc, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed to kill process")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_error_if_sending_stdin_to_dead_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Spawn a process that will exit immediately, but is a valid process
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("0")];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process")
assert(proc, "Missing proc")
// Wait briefly to ensure the process dies
$wait_fn()
local f = distant.utils.wrap_async(proc.write_stdin_async, $schedule_fn)
local err
f(proc, "some text\n", function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_support_sending_stdin_to_spawned_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![ECHO_STDIN_TO_STDOUT_SH.to_str().unwrap().to_string()];
let result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, proc
f(session, { cmd = $cmd, args = $args }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed spawning process")
assert(proc, "Missing proc")
local f = distant.utils.wrap_async(proc.write_stdin_async, $schedule_fn)
local err
f(proc, "some text\n", function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed writing stdin")
local f = distant.utils.wrap_async(proc.read_stdout_async, $schedule_fn)
local err, stdout
f(proc, function(success, res)
if success then
stdout = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed reading stdout")
assert(stdout == "some text\n", "Unexpected stdout received: " .. stdout)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,79 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_yield_error_if_fails_to_create_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Create a temporary path and add to it to ensure that there are
// extra components that don't exist to cause writing to fail
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("dir").child("test-file");
let file_path = file.path().to_str().unwrap();
let data = b"some text".to_vec();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.write_file_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path, data = $data }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we didn't actually create the file
file.assert(predicate::path::missing());
}
#[rstest]
fn should_overwrite_existing_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("line 1").unwrap();
let file_path = file.path().to_str().unwrap();
let data = b"some text".to_vec();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.write_file_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path, data = $data }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we overwrite the file
file.assert("some text");
}

@ -0,0 +1,79 @@
use crate::common::{fixtures::*, lua, poll, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_yield_error_if_fails_to_create_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
// Create a temporary path and add to it to ensure that there are
// extra components that don't exist to cause writing to fail
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("dir").child("test-file");
let file_path = file.path().to_str().unwrap();
let text = "some text".to_string();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.write_file_text_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path, data = $text }, function(success, res)
if not success then
err = res
end
end)
assert(err, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we didn't actually create the file
file.assert(predicate::path::missing());
}
#[rstest]
fn should_overwrite_existing_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let schedule_fn = poll::make_function(&lua).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("line 1").unwrap();
let file_path = file.path().to_str().unwrap();
let text = "some text".to_string();
let result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.write_file_text_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err
f(session, { path = $file_path, data = $text }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we appended to the file
file.assert("some text");
}

@ -0,0 +1,2 @@
mod r#async;
mod sync;

@ -0,0 +1,60 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_yield_error_if_fails_to_create_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Create a temporary path and add to it to ensure that there are
// extra components that don't exist to cause writing to fail
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("dir").child("test-file");
let file_path = file.path().to_str().unwrap();
let data = b"some text".to_vec();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.append_file, session, {
path = $file_path,
data = $data
})
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we didn't actually create the file
file.assert(predicate::path::missing());
}
#[rstest]
fn should_append_data_to_existing_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("line 1").unwrap();
let file_path = file.path().to_str().unwrap();
let data = b"some text".to_vec();
let result = lua
.load(chunk! {
local session = $new_session()
session:append_file({
path = $file_path,
data = $data
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we appended to the file
file.assert("line 1some text");
}

@ -0,0 +1,60 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_yield_error_if_fails_to_create_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Create a temporary path and add to it to ensure that there are
// extra components that don't exist to cause writing to fail
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("dir").child("test-file");
let file_path = file.path().to_str().unwrap();
let text = "some text".to_string();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.append_file_text, session, {
path = $file_path,
data = $text
})
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we didn't actually create the file
file.assert(predicate::path::missing());
}
#[rstest]
fn should_append_data_to_existing_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("line 1").unwrap();
let file_path = file.path().to_str().unwrap();
let text = "some text".to_string();
let result = lua
.load(chunk! {
local session = $new_session()
session:append_file_text({
path = $file_path,
data = $text
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we appended to the file
file.assert("line 1some text");
}

@ -0,0 +1,161 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_send_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
let dst = temp.child("dst");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.copy, session, {
src = $src_path,
dst = $dst_path
})
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that destination does not exist
dst.assert(predicate::path::missing());
}
#[rstest]
fn should_support_copying_an_entire_directory(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.create_dir_all().unwrap();
let src_file = src.child("file");
src_file.write_str("some contents").unwrap();
let dst = temp.child("dst");
let dst_file = dst.child("file");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:copy({
src = $src_path,
dst = $dst_path
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we have source and destination directories and associated contents
src.assert(predicate::path::is_dir());
src_file.assert(predicate::path::is_file());
dst.assert(predicate::path::is_dir());
dst_file.assert(predicate::path::eq_file(src_file.path()));
}
#[rstest]
fn should_support_copying_an_empty_directory(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.create_dir_all().unwrap();
let dst = temp.child("dst");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:copy({
src = $src_path,
dst = $dst_path
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we still have source and destination directories
src.assert(predicate::path::is_dir());
dst.assert(predicate::path::is_dir());
}
#[rstest]
fn should_support_copying_a_directory_that_only_contains_directories(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.create_dir_all().unwrap();
let src_dir = src.child("dir");
src_dir.create_dir_all().unwrap();
let dst = temp.child("dst");
let dst_dir = dst.child("dir");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:copy({
src = $src_path,
dst = $dst_path
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we have source and destination directories and associated contents
src.assert(predicate::path::is_dir().name("src"));
src_dir.assert(predicate::path::is_dir().name("src/dir"));
dst.assert(predicate::path::is_dir().name("dst"));
dst_dir.assert(predicate::path::is_dir().name("dst/dir"));
}
#[rstest]
fn should_support_copying_a_single_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.write_str("some text").unwrap();
let dst = temp.child("dst");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:copy({
src = $src_path,
dst = $dst_path
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we still have source and that destination has source's contents
src.assert(predicate::path::is_file());
dst.assert(predicate::path::eq_file(src.path()));
}

@ -0,0 +1,91 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
// /root/
// /root/file1
// /root/link1 -> /root/sub1/file2
// /root/sub1/
// /root/sub1/file2
fn setup_dir() -> assert_fs::TempDir {
let root_dir = assert_fs::TempDir::new().unwrap();
root_dir.child("file1").touch().unwrap();
let sub1 = root_dir.child("sub1");
sub1.create_dir_all().unwrap();
let file2 = sub1.child("file2");
file2.touch().unwrap();
let link1 = root_dir.child("link1");
link1.symlink_to_file(file2.path()).unwrap();
root_dir
}
#[rstest]
fn should_send_error_if_fails(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Make a path that has multiple non-existent components
// so the creation will fail
let root_dir = setup_dir();
let path = root_dir.path().join("nested").join("new-dir");
let path_str = path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.create_dir, session, { path = $path_str })
assert(not status, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that the directory was not actually created
assert!(!path.exists(), "Path unexpectedly exists");
}
#[rstest]
fn should_send_ok_when_successful(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let root_dir = setup_dir();
let path = root_dir.path().join("new-dir");
let path_str = path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:create_dir({ path = $path_str })
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that the directory was actually created
assert!(path.exists(), "Directory not created");
}
#[rstest]
fn should_support_creating_multiple_dir_components(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let root_dir = setup_dir();
let path = root_dir.path().join("nested").join("new-dir");
let path_str = path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:create_dir({ path = $path_str, all = true })
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that the directory was actually created
assert!(path.exists(), "Directory not created");
}

@ -0,0 +1,43 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
#[rstest]
fn should_send_true_if_path_exists(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.touch().unwrap();
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local exists = session:exists({ path = $file_path })
assert(exists, "File unexpectedly missing")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_send_false_if_path_does_not_exist(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local exists = session:exists({ path = $file_path })
assert(not exists, "File unexpectedly found")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,152 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
#[rstest]
fn should_send_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.metadata, session, { path = $file_path })
assert(not status, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_metadata_on_file_if_exists(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.write_str("some text").unwrap();
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local metadata = session:metadata({ path = $file_path })
assert(not metadata.canonicalized_path, "Unexpectedly got canonicalized path")
assert(metadata.file_type == "file", "Got wrong file type: " .. metadata.file_type)
assert(metadata.len == 9, "Got wrong len: " .. metadata.len)
assert(not metadata.readonly, "Unexpectedly readonly")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_metadata_on_dir_if_exists(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let dir = temp.child("dir");
dir.create_dir_all().unwrap();
let dir_path = dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local metadata = session:metadata({ path = $dir_path })
assert(not metadata.canonicalized_path, "Unexpectedly got canonicalized path")
assert(metadata.file_type == "dir", "Got wrong file type: " .. metadata.file_type)
assert(not metadata.readonly, "Unexpectedly readonly")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_metadata_on_symlink_if_exists(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.write_str("some text").unwrap();
let symlink = temp.child("link");
symlink.symlink_to_file(file.path()).unwrap();
let symlink_path = symlink.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local metadata = session:metadata({ path = $symlink_path })
assert(not metadata.canonicalized_path, "Unexpectedly got canonicalized path")
assert(metadata.file_type == "symlink", "Got wrong file type: " .. metadata.file_type)
assert(not metadata.readonly, "Unexpectedly readonly")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_include_canonicalized_path_if_flag_specified(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.write_str("some text").unwrap();
let file_path = file.path().canonicalize().unwrap();
let file_path_str = file_path.to_str().unwrap();
let symlink = temp.child("link");
symlink.symlink_to_file(file.path()).unwrap();
let symlink_path = symlink.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local metadata = session:metadata({
path = $symlink_path,
canonicalize = true,
})
assert(
metadata.canonicalized_path == $file_path_str,
"Got wrong canonicalized path: " .. metadata.canonicalized_path
)
assert(metadata.file_type == "symlink", "Got wrong file type: " .. metadata.file_type)
assert(not metadata.readonly, "Unexpectedly readonly")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_resolve_file_type_of_symlink_if_flag_specified(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.write_str("some text").unwrap();
let symlink = temp.child("link");
symlink.symlink_to_file(file.path()).unwrap();
let symlink_path = symlink.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local metadata = session:metadata({
path = $symlink_path,
resolve_file_type = true,
})
assert(metadata.file_type == "file", "Got wrong file type: " .. metadata.file_type)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,14 @@
mod append_file;
mod append_file_text;
mod copy;
mod create_dir;
mod exists;
mod metadata;
mod read_dir;
mod read_file;
mod read_file_text;
mod remove;
mod rename;
mod spawn;
mod write_file;
mod write_file_text;

@ -0,0 +1,249 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
// /root/
// /root/file1
// /root/link1 -> /root/sub1/file2
// /root/sub1/
// /root/sub1/file2
fn setup_dir() -> assert_fs::TempDir {
let root_dir = assert_fs::TempDir::new().unwrap();
root_dir.child("file1").touch().unwrap();
let sub1 = root_dir.child("sub1");
sub1.create_dir_all().unwrap();
let file2 = sub1.child("file2");
file2.touch().unwrap();
let link1 = root_dir.child("link1");
link1.symlink_to_file(file2.path()).unwrap();
root_dir
}
#[rstest]
fn should_return_error_if_directory_does_not_exist(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let dir = temp.child("test-dir");
let dir_path = dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.read_dir, session, { path = $dir_path })
assert(not status, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_have_depth_default_to_1(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local tbl = session:read_dir({ path = $root_dir_path })
local entries = tbl.entries
assert(entries[1].file_type == "file", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == "file1", "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 1, "Wrong depth")
assert(entries[2].file_type == "symlink", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == "link1", "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "dir", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == "sub1", "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_depth_limits(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local tbl = session:read_dir({ path = $root_dir_path, depth = 1 })
local entries = tbl.entries
assert(entries[1].file_type == "file", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == "file1", "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 1, "Wrong depth")
assert(entries[2].file_type == "symlink", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == "link1", "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "dir", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == "sub1", "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_unlimited_depth_using_zero(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local tbl = session:read_dir({ path = $root_dir_path, depth = 0 })
local entries = tbl.entries
assert(entries[1].file_type == "file", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == "file1", "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 1, "Wrong depth")
assert(entries[2].file_type == "symlink", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == "link1", "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "dir", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == "sub1", "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
assert(entries[4].file_type == "file", "Wrong file type: " .. entries[4].file_type)
assert(entries[4].path == "sub1/file2", "Wrong path: " .. entries[4].path)
assert(entries[4].depth == 2, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_including_directory_in_returned_entries(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let root_dir_canonicalized_path = root_dir.path().canonicalize().unwrap();
let root_dir_canonicalized_path_str = root_dir_canonicalized_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local tbl = session:read_dir({ path = $root_dir_path, include_root = true })
local entries = tbl.entries
assert(entries[1].file_type == "dir", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == $root_dir_canonicalized_path_str, "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 0, "Wrong depth")
assert(entries[2].file_type == "file", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == "file1", "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "symlink", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == "link1", "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
assert(entries[4].file_type == "dir", "Wrong file type: " .. entries[4].file_type)
assert(entries[4].path == "sub1", "Wrong path: " .. entries[4].path)
assert(entries[4].depth == 1, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_returning_absolute_paths(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let root_dir_canonicalized_path = root_dir.path().canonicalize().unwrap();
let file1_path = root_dir_canonicalized_path.join("file1");
let link1_path = root_dir_canonicalized_path.join("link1");
let sub1_path = root_dir_canonicalized_path.join("sub1");
let file1_path_str = file1_path.to_str().unwrap();
let link1_path_str = link1_path.to_str().unwrap();
let sub1_path_str = sub1_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local tbl = session:read_dir({ path = $root_dir_path, absolute = true })
local entries = tbl.entries
assert(entries[1].file_type == "file", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == $file1_path_str, "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 1, "Wrong depth")
assert(entries[2].file_type == "symlink", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == $link1_path_str, "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "dir", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == $sub1_path_str, "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_returning_canonicalized_paths(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Create directory with some nested items
let root_dir = setup_dir();
let root_dir_path = root_dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local tbl = session:read_dir({ path = $root_dir_path, canonicalize = true })
local entries = tbl.entries
assert(entries[1].file_type == "file", "Wrong file type: " .. entries[1].file_type)
assert(entries[1].path == "file1", "Wrong path: " .. entries[1].path)
assert(entries[1].depth == 1, "Wrong depth")
assert(entries[2].file_type == "symlink", "Wrong file type: " .. entries[2].file_type)
assert(entries[2].path == "sub1/file2", "Wrong path: " .. entries[2].path)
assert(entries[2].depth == 1, "Wrong depth")
assert(entries[3].file_type == "dir", "Wrong file type: " .. entries[3].file_type)
assert(entries[3].path == "sub1", "Wrong path: " .. entries[3].path)
assert(entries[3].depth == 1, "Wrong depth")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,51 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
#[rstest]
fn should_return_error_if_fails_to_read_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("missing-file");
let file_path = file.path();
let file_path_str = file_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.read_file, session, { path = $file_path_str })
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_file_contents_as_byte_list(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("abcd").unwrap();
let file_path = file.path();
let file_path_str = file_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local contents = session:read_file({ path = $file_path_str })
// abcd -> {97, 98, 99, 100}
assert(type(contents) == "table", "Wrong content type: " .. type(contents))
assert(contents[1] == 97, "Unexpected first byte: " .. contents[1])
assert(contents[2] == 98, "Unexpected second byte: " .. contents[2])
assert(contents[3] == 99, "Unexpected third byte: " .. contents[3])
assert(contents[4] == 100, "Unexpected fourth byte: " .. contents[4])
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,45 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use rstest::*;
#[rstest]
fn should_return_error_if_fails_to_read_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("missing-file");
let file_path = file.path();
let file_path_str = file_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.read_file_text, session, { path = $file_path_str })
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_file_contents_as_text(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("some file contents").unwrap();
let file_path = file.path();
let file_path_str = file_path.to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local contents = session:read_file_text({ path = $file_path_str })
assert(contents == "some file contents", "Unexpected file contents: " .. contents)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,94 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_return_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("missing-file");
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.remove, session, { path = $file_path })
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that path does not exist
file.assert(predicate::path::missing());
}
#[rstest]
fn should_support_deleting_a_directory(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let dir = temp.child("dir");
dir.create_dir_all().unwrap();
let dir_path = dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:remove({ path = $dir_path })
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that path does not exist
dir.assert(predicate::path::missing());
}
#[rstest]
fn should_delete_nonempty_directory_if_force_is_true(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let dir = temp.child("dir");
dir.create_dir_all().unwrap();
dir.child("file").touch().unwrap();
let dir_path = dir.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:remove({ path = $dir_path, force = true })
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that path does not exist
dir.assert(predicate::path::missing());
}
#[rstest]
fn should_support_deleting_a_single_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("some-file");
file.touch().unwrap();
let file_path = file.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:remove({ path = $file_path })
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that path does not exist
file.assert(predicate::path::missing());
}

@ -0,0 +1,97 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_return_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
let dst = temp.child("dst");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.rename, session, {
src = $src_path,
dst = $dst_path,
})
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also, verify that destination does not exist
dst.assert(predicate::path::missing());
}
#[rstest]
fn should_support_renaming_an_entire_directory(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.create_dir_all().unwrap();
let src_file = src.child("file");
src_file.write_str("some contents").unwrap();
let dst = temp.child("dst");
let dst_file = dst.child("file");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:rename({
src = $src_path,
dst = $dst_path,
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we moved the contents
src.assert(predicate::path::missing());
src_file.assert(predicate::path::missing());
dst.assert(predicate::path::is_dir());
dst_file.assert("some contents");
}
#[rstest]
fn should_support_renaming_a_single_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let src = temp.child("src");
src.write_str("some text").unwrap();
let dst = temp.child("dst");
let src_path = src.path().to_str().unwrap();
let dst_path = dst.path().to_str().unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
session:rename({
src = $src_path,
dst = $dst_path,
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Verify that we moved the file
src.assert(predicate::path::missing());
dst.assert("some text");
}

@ -0,0 +1,288 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use once_cell::sync::Lazy;
use rstest::*;
static TEMP_SCRIPT_DIR: Lazy<assert_fs::TempDir> = Lazy::new(|| assert_fs::TempDir::new().unwrap());
static SCRIPT_RUNNER: Lazy<String> = Lazy::new(|| String::from("bash"));
static ECHO_ARGS_TO_STDOUT_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stdout.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
printf "%s" "$*"
"#
))
.unwrap();
script
});
static ECHO_ARGS_TO_STDERR_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_args_to_stderr.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
printf "%s" "$*" 1>&2
"#
))
.unwrap();
script
});
static ECHO_STDIN_TO_STDOUT_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("echo_stdin_to_stdout.sh");
script
.write_str(indoc::indoc!(
r#"
#/usr/bin/env bash
while IFS= read; do echo "$REPLY"; done
"#
))
.unwrap();
script
});
static SLEEP_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
let script = TEMP_SCRIPT_DIR.child("sleep.sh");
script
.write_str(indoc::indoc!(
r#"
#!/usr/bin/env bash
sleep "$1"
"#
))
.unwrap();
script
});
static DOES_NOT_EXIST_BIN: Lazy<assert_fs::fixture::ChildPath> =
Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin"));
#[rstest]
fn should_return_error_on_failure(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = DOES_NOT_EXIST_BIN.to_str().unwrap().to_string();
let args: Vec<String> = Vec::new();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.spawn, session, {
cmd = $cmd,
args = $args
})
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_back_process_on_success(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string()];
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args })
assert(proc.id >= 0, "Invalid process returned")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_return_process_that_can_retrieve_stdout(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![
ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string(),
String::from("some stdout"),
];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args })
// Wait briefly to ensure the process sends stdout
$wait_fn()
local stdout = proc:read_stdout()
assert(stdout == "some stdout", "Unexpected stdout: " .. stdout)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_return_process_that_can_retrieve_stderr(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![
ECHO_ARGS_TO_STDERR_SH.to_str().unwrap().to_string(),
String::from("some stderr"),
];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args })
// Wait briefly to ensure the process sends stdout
$wait_fn()
local stderr = proc:read_stderr()
assert(stderr == "some stderr", "Unexpected stderr: " .. stderr)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_error_when_killing_dead_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Spawn a process that will exit immediately, but is a valid process
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("0")];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args })
// Wait briefly to ensure the process dies
$wait_fn()
local status, _ = pcall(proc.kill, proc)
assert(not status, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_support_killing_processing(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("1")];
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args })
proc:kill()
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
#[rstest]
fn should_return_error_if_sending_stdin_to_dead_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Spawn a process that will exit immediately, but is a valid process
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("0")];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args })
// Wait briefly to ensure the process dies
$wait_fn()
local status, _ = pcall(proc.write_stdin, proc, "some text")
assert(not status, "Unexpectedly succeeded")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
// with / but thinks it's on windows and is providing \
#[rstest]
#[cfg_attr(windows, ignore)]
fn should_support_sending_stdin_to_spawned_process(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let cmd = SCRIPT_RUNNER.to_string();
let args = vec![ECHO_STDIN_TO_STDOUT_SH.to_str().unwrap().to_string()];
let wait_fn = lua
.create_function(|_, ()| {
std::thread::sleep(std::time::Duration::from_millis(50));
Ok(())
})
.unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local proc = session:spawn({ cmd = $cmd, args = $args })
proc:write_stdin("some text\n")
// Wait briefly to ensure the process echoes stdin
$wait_fn()
local stdout = proc:read_stdout()
assert(stdout == "some text\n", "Unexpected stdin sent: " .. stdout)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -0,0 +1,60 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_yield_error_if_fails_to_create_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Create a temporary path and add to it to ensure that there are
// extra components that don't exist to cause writing to fail
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("dir").child("test-file");
let file_path = file.path().to_str().unwrap();
let data = b"some text".to_vec();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.write_file, session, {
path = $file_path,
data = $data
})
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we didn't actually create the file
file.assert(predicate::path::missing());
}
#[rstest]
fn should_overwrite_existing_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("line 1").unwrap();
let file_path = file.path().to_str().unwrap();
let data = b"some text".to_vec();
let result = lua
.load(chunk! {
local session = $new_session()
session:write_file({
path = $file_path,
data = $data
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we overwrite the file
file.assert("some text");
}

@ -0,0 +1,60 @@
use crate::common::{fixtures::*, lua, session};
use assert_fs::prelude::*;
use mlua::chunk;
use predicates::prelude::*;
use rstest::*;
#[rstest]
fn should_yield_error_if_fails_to_create_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
// Create a temporary path and add to it to ensure that there are
// extra components that don't exist to cause writing to fail
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("dir").child("test-file");
let file_path = file.path().to_str().unwrap();
let text = "some text".to_string();
let result = lua
.load(chunk! {
local session = $new_session()
local status, _ = pcall(session.write_file_text, session, {
path = $file_path,
data = $text
})
assert(not status, "Unexpectedly succeeded!")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we didn't actually create the file
file.assert(predicate::path::missing());
}
#[rstest]
fn should_overwrite_existing_file(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("line 1").unwrap();
let file_path = file.path().to_str().unwrap();
let text = "some text".to_string();
let result = lua
.load(chunk! {
local session = $new_session()
session:write_file_text({
path = $file_path,
data = $text
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we appended to the file
file.assert("some text");
}

@ -0,0 +1,11 @@
[target.x86_64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
[target.aarch64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]

@ -0,0 +1,41 @@
[package]
name = "distant-lua"
description = "Lua bindings to the distant Rust crates"
categories = ["api-bindings", "network-programming"]
keywords = ["api", "async"]
version = "0.15.0-alpha.6"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"
homepage = "https://github.com/chipsenkbeil/distant"
repository = "https://github.com/chipsenkbeil/distant"
readme = "README.md"
license = "MIT OR Apache-2.0"
[lib]
crate-type = ["cdylib"]
[features]
default = ["lua51", "vendored"]
lua54 = ["mlua/lua54"]
lua53 = ["mlua/lua53"]
lua52 = ["mlua/lua52"]
lua51 = ["mlua/lua51"]
luajit = ["mlua/luajit"]
vendored = ["mlua/vendored"]
[dependencies]
distant-core = { version = "=0.15.0-alpha.6", path = "../distant-core" }
distant-ssh2 = { version = "=0.15.0-alpha.6", features = ["serde"], path = "../distant-ssh2" }
futures = "0.3.17"
log = "0.4.14"
mlua = { version = "0.6.5", features = ["async", "macros", "module", "serialize"] }
once_cell = "1.8.0"
oorandom = "11.1.3"
paste = "1.0.5"
serde = { version = "1.0.130", features = ["derive"] }
simplelog = "0.10.2"
tokio = { version = "1.12.0", features = ["macros", "time"] }
whoami = "1.1.4"
[dev-dependencies]
rstest = "0.11.0"

@ -0,0 +1,65 @@
# Distant Lua (module)
Contains the Lua module wrapper around several distant libraries
including:
1. **distant-core**
2. **distant-ssh2**
## Building
*Compilation MUST be done within this directory! This crate depends on
.cargo/config.toml settings, which are only used when built from within this
directory.*
```bash
# Outputs a library file (*.so for Linux, *.dylib for MacOS)
cargo build --release
```
## Examples
Rename `libdistant_lua.so` or `libdistant_lua.dylib` to `distant_lua.so`
(yes, **.so** for **.dylib**) and place the library in your Lua path.
```lua
local distant = require("distant_lua")
-- Distant functions are async by design and need to be wrapped in a coroutine
-- in order to be used
local thread = coroutine.wrap(distant.launch)
-- Initialize the thread
thread({ host = "127.0.0.1" })
-- Continually check if launch has completed
local res
while true do
res = thread()
if res ~= distant.PENDING then
break
end
end
```
## Tests
Tests are run in a separate crate due to linking described here:
[khvzak/mlua#79](https://github.com/khvzak/mlua/issues/79). You **must** build
this module prior to running the tests!
```bash
# From root of repository
(cd distant-lua-tests && cargo test --release)
```
## License
This project is licensed under either of
Apache License, Version 2.0, (LICENSE-APACHE or
[apache-license][apache-license]) MIT license (LICENSE-MIT or
[mit-license][mit-license]) at your option.
[apache-license]: http://www.apache.org/licenses/LICENSE-2.0
[mit-license]: http://opensource.org/licenses/MIT

@ -0,0 +1,31 @@
use mlua::prelude::*;
/// to_value!<'a, T: Serialize + ?Sized>(lua: &'a Lua, t: &T) -> Result<Value<'a>>
///
/// Converts to a Lua value using options specific to this module.
macro_rules! to_value {
($lua:expr, $x:expr) => {{
use mlua::{prelude::*, LuaSerdeExt};
let options = LuaSerializeOptions::new()
.serialize_none_to_null(false)
.serialize_unit_to_null(false);
$lua.to_value_with($x, options)
}};
}
mod log;
mod runtime;
mod session;
mod utils;
#[mlua::lua_module]
fn distant_lua(lua: &Lua) -> LuaResult<LuaTable> {
let exports = lua.create_table()?;
exports.set("PENDING", utils::pending(lua)?)?;
exports.set("log", log::make_log_tbl(lua)?)?;
exports.set("session", session::make_session_tbl(lua)?)?;
exports.set("utils", utils::make_utils_tbl(lua)?)?;
Ok(exports)
}

@ -0,0 +1,114 @@
use mlua::prelude::*;
use serde::{Deserialize, Serialize};
use simplelog::{
ColorChoice, CombinedLogger, ConfigBuilder, LevelFilter, SharedLogger, TermLogger,
TerminalMode, WriteLogger,
};
use std::{fs::File, path::PathBuf};
macro_rules! set_log_fn {
($lua:expr, $tbl:expr, $name:ident) => {
$tbl.set(
stringify!($name),
$lua.create_function(|_, msg: String| {
::log::$name!("{}", msg);
Ok(())
})?,
)?;
};
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum LogLevel {
Off,
Error,
Warn,
Info,
Debug,
Trace,
}
impl From<LogLevel> for LevelFilter {
fn from(level: LogLevel) -> Self {
match level {
LogLevel::Off => Self::Off,
LogLevel::Error => Self::Error,
LogLevel::Warn => Self::Warn,
LogLevel::Info => Self::Info,
LogLevel::Debug => Self::Debug,
LogLevel::Trace => Self::Trace,
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
struct LogOpts {
/// Indicating whether or not to log to terminal
terminal: bool,
/// Path to file to store logs
file: Option<PathBuf>,
/// Base level at which to write logs
/// (e.g. if debug then trace would not be logged)
level: LogLevel,
}
impl Default for LogOpts {
fn default() -> Self {
Self {
terminal: false,
file: None,
level: LogLevel::Warn,
}
}
}
fn init_logger(opts: LogOpts) -> LuaResult<()> {
let mut loggers: Vec<Box<dyn SharedLogger>> = Vec::new();
let config = ConfigBuilder::new()
.add_filter_allow_str("distant_core")
.add_filter_allow_str("distant_ssh2")
.add_filter_allow_str("distant_lua")
.build();
if opts.terminal {
loggers.push(TermLogger::new(
opts.level.into(),
config.clone(),
TerminalMode::Mixed,
ColorChoice::Auto,
));
}
if let Some(path) = opts.file {
loggers.push(WriteLogger::new(
opts.level.into(),
config,
File::create(path)?,
));
}
CombinedLogger::init(loggers).to_lua_err()?;
Ok(())
}
/// Makes a Lua table containing the log functions
pub fn make_log_tbl(lua: &Lua) -> LuaResult<LuaTable> {
let tbl = lua.create_table()?;
tbl.set(
"init",
lua.create_function(|lua, opts: LuaValue| init_logger(lua.from_value(opts)?))?,
)?;
set_log_fn!(lua, tbl, error);
set_log_fn!(lua, tbl, warn);
set_log_fn!(lua, tbl, info);
set_log_fn!(lua, tbl, debug);
set_log_fn!(lua, tbl, trace);
Ok(tbl)
}

@ -0,0 +1,38 @@
use futures::{FutureExt, TryFutureExt};
use mlua::prelude::*;
use once_cell::sync::OnceCell;
use std::future::Future;
/// Retrieves the global runtime, initializing it if not initialized, and returning
/// an error if failed to initialize
pub fn get_runtime() -> LuaResult<&'static tokio::runtime::Runtime> {
static RUNTIME: OnceCell<tokio::runtime::Runtime> = OnceCell::new();
RUNTIME.get_or_try_init(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|x| x.to_lua_err())
})
}
/// Blocks using the global runtime for a future that returns `LuaResult<T>`
pub fn block_on<F, T>(future: F) -> LuaResult<T>
where
F: Future<Output = Result<T, LuaError>>,
{
get_runtime()?.block_on(future)
}
/// Spawns a task on the global runtime for a future that returns a `LuaResult<T>`
pub fn spawn<F, T>(f: F) -> impl Future<Output = LuaResult<T>>
where
F: Future<Output = Result<T, LuaError>> + Send + 'static,
T: Send + 'static,
{
futures::future::ready(get_runtime()).and_then(|rt| {
rt.spawn(f).map(|result| match result {
Ok(x) => x.to_lua_err(),
Err(x) => Err(x).to_lua_err(),
})
})
}

@ -0,0 +1,280 @@
use crate::{runtime, utils};
use distant_core::{
SecretKey32, Session as DistantSession, SessionChannel, XChaCha20Poly1305Codec,
};
use distant_ssh2::{IntoDistantSessionOpts, Ssh2Session};
use mlua::{prelude::*, LuaSerdeExt, UserData, UserDataFields, UserDataMethods};
use once_cell::sync::Lazy;
use paste::paste;
use std::{collections::HashMap, io, sync::RwLock};
/// Makes a Lua table containing the session functions
pub fn make_session_tbl(lua: &Lua) -> LuaResult<LuaTable> {
let tbl = lua.create_table()?;
// get_by_id(id: usize) -> Option<Session>
tbl.set(
"get_by_id",
lua.create_function(|_, id: usize| {
let exists = has_session(id)?;
if exists {
Ok(Some(Session::new(id)))
} else {
Ok(None)
}
})?,
)?;
// launch(opts: LaunchOpts) -> Session
tbl.set(
"launch",
lua.create_function(|lua, opts: LuaValue| {
let opts = LaunchOpts::from_lua(opts, lua)?;
runtime::block_on(Session::launch(opts))
})?,
)?;
// connect_async(opts: ConnectOpts) -> Future<Session>
tbl.set(
"connect_async",
lua.create_async_function(|lua, opts: LuaValue| async move {
let opts = ConnectOpts::from_lua(opts, lua)?;
runtime::spawn(Session::connect(opts)).await
})?,
)?;
// connect(opts: ConnectOpts) -> Session
tbl.set(
"connect",
lua.create_function(|lua, opts: LuaValue| {
let opts = ConnectOpts::from_lua(opts, lua)?;
runtime::block_on(Session::connect(opts))
})?,
)?;
Ok(tbl)
}
/// try_timeout!(timeout: Duration, Future<Output = Result<T, E>>) -> LuaResult<T>
macro_rules! try_timeout {
($timeout:expr, $f:expr) => {{
use futures::future::FutureExt;
use mlua::prelude::*;
let timeout: std::time::Duration = $timeout;
crate::runtime::spawn(async move {
let fut = ($f).fuse();
let sleep = tokio::time::sleep(timeout).fuse();
tokio::select! {
_ = sleep => {
let err = std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("Reached timeout of {}s", timeout.as_secs_f32())
);
Err(err.to_lua_err())
}
res = fut => {
res.to_lua_err()
}
}
})
.await
}};
}
mod api;
mod opts;
mod proc;
use opts::Mode;
pub use opts::{ConnectOpts, LaunchOpts};
use proc::{RemoteLspProcess, RemoteProcess};
/// Contains mapping of id -> session for use in maintaining active sessions
static SESSION_MAP: Lazy<RwLock<HashMap<usize, DistantSession>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
fn has_session(id: usize) -> LuaResult<bool> {
Ok(SESSION_MAP
.read()
.map_err(|x| x.to_string().to_lua_err())?
.contains_key(&id))
}
fn get_session_channel(id: usize) -> LuaResult<SessionChannel> {
let lock = SESSION_MAP.read().map_err(|x| x.to_string().to_lua_err())?;
let session = lock.get(&id).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotConnected,
format!("No session connected with id {}", id),
)
.to_lua_err()
})?;
Ok(session.clone_channel())
}
/// Holds a reference to the session to perform remote operations
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Session {
id: usize,
}
impl Session {
/// Creates a new session referencing the given distant session with the specified id
pub fn new(id: usize) -> Self {
Self { id }
}
/// Launches a new distant session on a remote machine
pub async fn launch(opts: LaunchOpts<'_>) -> LuaResult<Self> {
let LaunchOpts {
host,
mode,
handler,
ssh,
timeout,
} = opts;
// First, establish a connection to an SSH server
let mut ssh_session = Ssh2Session::connect(host, ssh).to_lua_err()?;
// Second, authenticate with the server
ssh_session.authenticate(handler).await.to_lua_err()?;
// Third, convert our ssh session into a distant session based on desired method
let session = match mode {
Mode::Distant => ssh_session
.into_distant_session(IntoDistantSessionOpts {
timeout,
..Default::default()
})
.await
.to_lua_err()?,
Mode::Ssh => ssh_session.into_ssh_client_session().to_lua_err()?,
};
// Fourth, store our current session in our global map and then return a reference
let id = utils::rand_u32()? as usize;
SESSION_MAP
.write()
.map_err(|x| x.to_string().to_lua_err())?
.insert(id, session);
Ok(Self::new(id))
}
/// Connects to an already-running remote distant server
pub async fn connect(opts: ConnectOpts) -> LuaResult<Self> {
let addr = tokio::net::lookup_host(format!("{}:{}", opts.host, opts.port))
.await
.to_lua_err()?
.next()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::AddrNotAvailable,
"Failed to resolve host & port",
)
})
.to_lua_err()?;
let key: SecretKey32 = opts.key.parse().to_lua_err()?;
let codec = XChaCha20Poly1305Codec::from(key);
let session = DistantSession::tcp_connect_timeout(addr, codec, opts.timeout)
.await
.to_lua_err()?;
let id = utils::rand_u32()? as usize;
SESSION_MAP
.write()
.map_err(|x| x.to_string().to_lua_err())?
.insert(id, session);
Ok(Self::new(id))
}
}
/// impl_methods!(methods: &mut M, name: Ident)
macro_rules! impl_methods {
($methods:expr, $name:ident) => {
impl_methods!($methods, $name, |_lua, data| {Ok(data)});
};
($methods:expr, $name:ident, |$lua:ident, $data:ident| $block:block) => {{
paste! {
$methods.add_method(stringify!([<$name:snake>]), |$lua, this, params: LuaValue| {
let params: api::[<$name:camel Params>] = $lua.from_value(params)?;
let $data = api::[<$name:snake>](get_session_channel(this.id)?, params)?;
#[allow(unused_braces)]
$block
});
$methods.add_async_method(stringify!([<$name:snake _async>]), |$lua, this, params: LuaValue| async move {
let rt = crate::runtime::get_runtime()?;
let params: api::[<$name:camel Params>] = $lua.from_value(params)?;
let $data = {
let tmp = rt.spawn(
api::[<$name:snake _async>](get_session_channel(this.id)?, params)
).await;
match tmp {
Ok(x) => x.to_lua_err(),
Err(x) => Err(x).to_lua_err(),
}
}?;
#[allow(unused_braces)]
$block
});
}
}};
}
impl UserData for Session {
fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, this| Ok(this.id));
}
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("is_active", |_, this, _: LuaValue| {
Ok(get_session_channel(this.id).is_ok())
});
impl_methods!(methods, append_file);
impl_methods!(methods, append_file_text);
impl_methods!(methods, copy);
impl_methods!(methods, create_dir);
impl_methods!(methods, exists);
impl_methods!(methods, metadata, |lua, m| { to_value!(lua, &m) });
impl_methods!(methods, read_dir, |lua, results| {
let (entries, errors) = results;
let tbl = lua.create_table()?;
tbl.set(
"entries",
entries
.iter()
.map(|x| to_value!(lua, x))
.collect::<LuaResult<Vec<LuaValue>>>()?,
)?;
tbl.set(
"errors",
errors
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)?;
Ok(tbl)
});
impl_methods!(methods, read_file);
impl_methods!(methods, read_file_text);
impl_methods!(methods, remove);
impl_methods!(methods, rename);
impl_methods!(methods, spawn, |_lua, proc| {
Ok(RemoteProcess::from_distant(proc))
});
impl_methods!(methods, spawn_lsp, |_lua, proc| {
Ok(RemoteLspProcess::from_distant(proc))
});
impl_methods!(methods, write_file);
impl_methods!(methods, write_file_text);
}
}

@ -0,0 +1,187 @@
use crate::runtime;
use distant_core::{
DirEntry, Error as Failure, Metadata, RemoteLspProcess, RemoteProcess, SessionChannel,
SessionChannelExt,
};
use mlua::prelude::*;
use once_cell::sync::Lazy;
use paste::paste;
use serde::Deserialize;
use std::{path::PathBuf, time::Duration};
static TENANT: Lazy<String> = Lazy::new(whoami::hostname);
/// Default depth for reading directory
const fn default_depth() -> usize {
1
}
// Default timeout in milliseconds (15 secs)
const fn default_timeout() -> u64 {
15000
}
macro_rules! make_api {
(
$name:ident,
$ret:ty,
{$($(#[$pmeta:meta])* $pname:ident: $ptype:ty),*},
|$channel:ident, $tenant:ident, $params:ident| $block:block $(,)?
) => {
paste! {
#[derive(Clone, Debug, Deserialize)]
pub struct [<$name:camel Params>] {
$($(#[$pmeta])* $pname: $ptype,)*
#[serde(default = "default_timeout")]
timeout: u64,
}
impl [<$name:camel Params>] {
fn to_timeout_duration(&self) -> Duration {
Duration::from_millis(self.timeout)
}
}
pub fn [<$name:snake>](
channel: SessionChannel,
params: [<$name:camel Params>],
) -> LuaResult<$ret> {
runtime::block_on([<$name:snake _async>](channel, params))
}
pub async fn [<$name:snake _async>](
channel: SessionChannel,
params: [<$name:camel Params>],
) -> LuaResult<$ret> {
try_timeout!(params.to_timeout_duration(), async move {
let f = |
mut $channel: SessionChannel,
$tenant: &'static str,
$params: [<$name:camel Params>]
| async move $block;
f(channel, TENANT.as_str(), params).await
})
}
}
};
}
make_api!(append_file, (), { path: PathBuf, data: Vec<u8> }, |channel, tenant, params| {
channel.append_file(tenant, params.path, params.data).await
});
make_api!(append_file_text, (), { path: PathBuf, data: String }, |channel, tenant, params| {
channel.append_file_text(tenant, params.path, params.data).await
});
make_api!(copy, (), { src: PathBuf, dst: PathBuf }, |channel, tenant, params| {
channel.copy(tenant, params.src, params.dst).await
});
make_api!(create_dir, (), { path: PathBuf, #[serde(default)] all: bool }, |channel, tenant, params| {
channel.create_dir(tenant, params.path, params.all).await
});
make_api!(
exists,
bool,
{ path: PathBuf },
|channel, tenant, params| { channel.exists(tenant, params.path).await }
);
make_api!(
metadata,
Metadata,
{
path: PathBuf,
#[serde(default)] canonicalize: bool,
#[serde(default)] resolve_file_type: bool
},
|channel, tenant, params| {
channel.metadata(
tenant,
params.path,
params.canonicalize,
params.resolve_file_type
).await
}
);
make_api!(
read_dir,
(Vec<DirEntry>, Vec<Failure>),
{
path: PathBuf,
#[serde(default = "default_depth")] depth: usize,
#[serde(default)] absolute: bool,
#[serde(default)] canonicalize: bool,
#[serde(default)] include_root: bool
},
|channel, tenant, params| {
channel.read_dir(
tenant,
params.path,
params.depth,
params.absolute,
params.canonicalize,
params.include_root,
).await
}
);
make_api!(
read_file,
Vec<u8>,
{ path: PathBuf },
|channel, tenant, params| { channel.read_file(tenant, params.path).await }
);
make_api!(
read_file_text,
String,
{ path: PathBuf },
|channel, tenant, params| { channel.read_file_text(tenant, params.path).await }
);
make_api!(
remove,
(),
{ path: PathBuf, #[serde(default)] force: bool },
|channel, tenant, params| { channel.remove(tenant, params.path, params.force).await }
);
make_api!(
rename,
(),
{ src: PathBuf, dst: PathBuf },
|channel, tenant, params| { channel.rename(tenant, params.src, params.dst).await }
);
make_api!(
spawn,
RemoteProcess,
{ cmd: String, args: Vec<String> },
|channel, tenant, params| { channel.spawn(tenant, params.cmd, params.args).await }
);
make_api!(
spawn_lsp,
RemoteLspProcess,
{ cmd: String, args: Vec<String> },
|channel, tenant, params| { channel.spawn_lsp(tenant, params.cmd, params.args).await }
);
make_api!(
write_file,
(),
{ path: PathBuf, data: Vec<u8> },
|channel, tenant, params| { channel.write_file(tenant, params.path, params.data).await }
);
make_api!(
write_file_text,
(),
{ path: PathBuf, data: String },
|channel, tenant, params| { channel.write_file_text(tenant, params.path, params.data).await }
);

@ -0,0 +1,208 @@
use distant_ssh2::{Ssh2AuthHandler, Ssh2SessionOpts};
use mlua::prelude::*;
use serde::Deserialize;
use std::{fmt, io, time::Duration};
#[derive(Clone, Debug, Default)]
pub struct ConnectOpts {
pub host: String,
pub port: u16,
pub key: String,
pub timeout: Duration,
}
impl<'lua> FromLua<'lua> for ConnectOpts {
fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
match lua_value {
LuaValue::Table(tbl) => Ok(Self {
host: tbl.get("host")?,
port: tbl.get("port")?,
key: tbl.get("key")?,
timeout: {
let milliseconds: u64 = tbl.get("timeout")?;
Duration::from_millis(milliseconds)
},
}),
LuaValue::Nil => Err(LuaError::FromLuaConversionError {
from: "Nil",
to: "ConnectOpts",
message: None,
}),
LuaValue::Boolean(_) => Err(LuaError::FromLuaConversionError {
from: "Boolean",
to: "ConnectOpts",
message: None,
}),
LuaValue::LightUserData(_) => Err(LuaError::FromLuaConversionError {
from: "LightUserData",
to: "ConnectOpts",
message: None,
}),
LuaValue::Integer(_) => Err(LuaError::FromLuaConversionError {
from: "Integer",
to: "ConnectOpts",
message: None,
}),
LuaValue::Number(_) => Err(LuaError::FromLuaConversionError {
from: "Number",
to: "ConnectOpts",
message: None,
}),
LuaValue::String(_) => Err(LuaError::FromLuaConversionError {
from: "String",
to: "ConnectOpts",
message: None,
}),
LuaValue::Function(_) => Err(LuaError::FromLuaConversionError {
from: "Function",
to: "ConnectOpts",
message: None,
}),
LuaValue::Thread(_) => Err(LuaError::FromLuaConversionError {
from: "Thread",
to: "ConnectOpts",
message: None,
}),
LuaValue::UserData(_) => Err(LuaError::FromLuaConversionError {
from: "UserData",
to: "ConnectOpts",
message: None,
}),
LuaValue::Error(_) => Err(LuaError::FromLuaConversionError {
from: "Error",
to: "ConnectOpts",
message: None,
}),
}
}
}
#[derive(Default)]
pub struct LaunchOpts<'a> {
pub host: String,
pub mode: Mode,
pub handler: Ssh2AuthHandler<'a>,
pub ssh: Ssh2SessionOpts,
pub timeout: Duration,
}
impl fmt::Debug for LaunchOpts<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LaunchOpts")
.field("host", &self.host)
.field("mode", &self.mode)
.field("handler", &"...")
.field("ssh", &self.ssh)
.field("timeout", &self.timeout)
.finish()
}
}
impl<'lua> FromLua<'lua> for LaunchOpts<'lua> {
fn from_lua(lua_value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
match lua_value {
LuaValue::Table(tbl) => Ok(Self {
host: tbl.get("host")?,
mode: lua.from_value(tbl.get("mode")?)?,
handler: Ssh2AuthHandler {
on_authenticate: {
let f: LuaFunction = tbl.get("on_authenticate")?;
Box::new(move |ev| {
let value = to_value!(lua, &ev)
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?;
f.call::<LuaValue, Vec<String>>(value)
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))
})
},
on_banner: {
let f: LuaFunction = tbl.get("on_banner")?;
Box::new(move |banner| {
let _ = f.call::<String, ()>(banner.to_string());
})
},
on_host_verify: {
let f: LuaFunction = tbl.get("on_host_verify")?;
Box::new(move |host| {
f.call::<String, bool>(host.to_string())
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))
})
},
on_error: {
let f: LuaFunction = tbl.get("on_error")?;
Box::new(move |err| {
let _ = f.call::<String, ()>(err.to_string());
})
},
},
ssh: lua.from_value(tbl.get("ssh")?)?,
timeout: {
let milliseconds: u64 = tbl.get("timeout")?;
Duration::from_millis(milliseconds)
},
}),
LuaValue::Nil => Err(LuaError::FromLuaConversionError {
from: "Nil",
to: "LaunchOpts",
message: None,
}),
LuaValue::Boolean(_) => Err(LuaError::FromLuaConversionError {
from: "Boolean",
to: "LaunchOpts",
message: None,
}),
LuaValue::LightUserData(_) => Err(LuaError::FromLuaConversionError {
from: "LightUserData",
to: "LaunchOpts",
message: None,
}),
LuaValue::Integer(_) => Err(LuaError::FromLuaConversionError {
from: "Integer",
to: "LaunchOpts",
message: None,
}),
LuaValue::Number(_) => Err(LuaError::FromLuaConversionError {
from: "Number",
to: "LaunchOpts",
message: None,
}),
LuaValue::String(_) => Err(LuaError::FromLuaConversionError {
from: "String",
to: "LaunchOpts",
message: None,
}),
LuaValue::Function(_) => Err(LuaError::FromLuaConversionError {
from: "Function",
to: "LaunchOpts",
message: None,
}),
LuaValue::Thread(_) => Err(LuaError::FromLuaConversionError {
from: "Thread",
to: "LaunchOpts",
message: None,
}),
LuaValue::UserData(_) => Err(LuaError::FromLuaConversionError {
from: "UserData",
to: "LaunchOpts",
message: None,
}),
LuaValue::Error(_) => Err(LuaError::FromLuaConversionError {
from: "Error",
to: "LaunchOpts",
message: None,
}),
}
}
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
Distant,
Ssh,
}
impl Default for Mode {
fn default() -> Self {
Self::Distant
}
}

@ -0,0 +1,194 @@
use crate::runtime;
use distant_core::{
RemoteLspProcess as DistantRemoteLspProcess, RemoteProcess as DistantRemoteProcess,
};
use mlua::{prelude::*, UserData, UserDataFields, UserDataMethods};
use once_cell::sync::Lazy;
use std::{collections::HashMap, io};
use tokio::sync::RwLock;
/// Contains mapping of id -> remote process for use in maintaining active processes
static PROC_MAP: Lazy<RwLock<HashMap<usize, DistantRemoteProcess>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
/// Contains mapping of id -> remote lsp process for use in maintaining active processes
static LSP_PROC_MAP: Lazy<RwLock<HashMap<usize, DistantRemoteLspProcess>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
macro_rules! with_proc {
($map_name:ident, $id:expr, $proc:ident -> $f:expr) => {{
let id = $id;
let mut lock = runtime::get_runtime()?.block_on($map_name.write());
let $proc = lock.get_mut(&id).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("No remote process found with id {}", id),
)
.to_lua_err()
})?;
$f
}};
}
macro_rules! with_proc_async {
($map_name:ident, $id:expr, $proc:ident -> $f:expr) => {{
let id = $id;
let mut lock = $map_name.write().await;
let $proc = lock.get_mut(&id).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("No remote process found with id {}", id),
)
.to_lua_err()
})?;
$f
}};
}
macro_rules! impl_process {
($name:ident, $type:ty, $map_name:ident) => {
#[derive(Copy, Clone, Debug)]
pub struct $name {
id: usize,
}
impl $name {
pub fn new(id: usize) -> Self {
Self { id }
}
pub fn from_distant(proc: $type) -> LuaResult<Self> {
let id = proc.id();
runtime::get_runtime()?.block_on($map_name.write()).insert(id, proc);
Ok(Self::new(id))
}
fn is_active(id: usize) -> LuaResult<bool> {
Ok(runtime::get_runtime()?.block_on($map_name.read()).contains_key(&id))
}
fn write_stdin(id: usize, data: String) -> LuaResult<()> {
runtime::block_on(Self::write_stdin_async(id, data))
}
async fn write_stdin_async(id: usize, data: String) -> LuaResult<()> {
with_proc_async!($map_name, id, proc -> {
proc.stdin
.as_mut()
.ok_or_else(|| {
io::Error::new(io::ErrorKind::BrokenPipe, "Stdin closed").to_lua_err()
})?
.write(data.as_str())
.await
.to_lua_err()
})
}
fn close_stdin(id: usize) -> LuaResult<()> {
with_proc!($map_name, id, proc -> {
let _ = proc.stdin.take();
Ok(())
})
}
fn read_stdout(id: usize) -> LuaResult<Option<String>> {
with_proc!($map_name, id, proc -> {
proc.stdout
.as_mut()
.ok_or_else(|| {
io::Error::new(io::ErrorKind::BrokenPipe, "Stdout closed").to_lua_err()
})?
.try_read()
.to_lua_err()
})
}
async fn read_stdout_async(id: usize) -> LuaResult<String> {
with_proc_async!($map_name, id, proc -> {
proc.stdout
.as_mut()
.ok_or_else(|| {
io::Error::new(io::ErrorKind::BrokenPipe, "Stdout closed").to_lua_err()
})?
.read()
.await
.to_lua_err()
})
}
fn read_stderr(id: usize) -> LuaResult<Option<String>> {
with_proc!($map_name, id, proc -> {
proc.stderr
.as_mut()
.ok_or_else(|| {
io::Error::new(io::ErrorKind::BrokenPipe, "Stderr closed").to_lua_err()
})?
.try_read()
.to_lua_err()
})
}
async fn read_stderr_async(id: usize) -> LuaResult<String> {
with_proc_async!($map_name, id, proc -> {
proc.stderr
.as_mut()
.ok_or_else(|| {
io::Error::new(io::ErrorKind::BrokenPipe, "Stderr closed").to_lua_err()
})?
.read()
.await
.to_lua_err()
})
}
fn kill(id: usize) -> LuaResult<()> {
runtime::block_on(Self::kill_async(id))
}
async fn kill_async(id: usize) -> LuaResult<()> {
with_proc_async!($map_name, id, proc -> {
proc.kill().await.to_lua_err()
})
}
fn abort(id: usize) -> LuaResult<()> {
with_proc!($map_name, id, proc -> {
Ok(proc.abort())
})
}
}
impl UserData for $name {
fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, this| Ok(this.id));
}
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("is_active", |_, this, ()| Self::is_active(this.id));
methods.add_method("close_stdin", |_, this, ()| Self::close_stdin(this.id));
methods.add_method("write_stdin", |_, this, data: String| {
Self::write_stdin(this.id, data)
});
methods.add_async_method("write_stdin_async", |_, this, data: String| {
runtime::spawn(Self::write_stdin_async(this.id, data))
});
methods.add_method("read_stdout", |_, this, ()| Self::read_stdout(this.id));
methods.add_async_method("read_stdout_async", |_, this, ()| {
runtime::spawn(Self::read_stdout_async(this.id))
});
methods.add_method("read_stderr", |_, this, ()| Self::read_stderr(this.id));
methods.add_async_method("read_stderr_async", |_, this, ()| {
runtime::spawn(Self::read_stderr_async(this.id))
});
methods.add_method("kill", |_, this, ()| Self::kill(this.id));
methods.add_async_method("kill_async", |_, this, ()| {
runtime::spawn(Self::kill_async(this.id))
});
methods.add_method("abort", |_, this, ()| Self::abort(this.id));
}
}
};
}
impl_process!(RemoteProcess, DistantRemoteProcess, PROC_MAP);
impl_process!(RemoteLspProcess, DistantRemoteLspProcess, LSP_PROC_MAP);

@ -0,0 +1,113 @@
use mlua::{chunk, prelude::*};
use once_cell::sync::OnceCell;
use oorandom::Rand32;
use std::{
sync::Mutex,
time::{SystemTime, SystemTimeError, UNIX_EPOCH},
};
/// Makes a Lua table containing the utils functions
pub fn make_utils_tbl(lua: &Lua) -> LuaResult<LuaTable> {
let tbl = lua.create_table()?;
tbl.set(
"nvim_wrap_async",
lua.create_function(|lua, async_fn| nvim_wrap_async(lua, async_fn))?,
)?;
tbl.set(
"wrap_async",
lua.create_function(|lua, (async_fn, schedule_fn)| wrap_async(lua, async_fn, schedule_fn))?,
)?;
tbl.set("rand_u32", lua.create_function(|_, ()| rand_u32())?)?;
Ok(tbl)
}
/// Specialty function that performs wrap_async using `vim.schedule` from neovim
pub fn nvim_wrap_async<'a>(lua: &'a Lua, async_fn: LuaFunction<'a>) -> LuaResult<LuaFunction<'a>> {
let schedule_fn = lua.load("vim.schedule").eval()?;
wrap_async(lua, async_fn, schedule_fn)
}
/// Wraps an async function and a scheduler function such that
/// a new function is returned that takes a callback when the async
/// function completes as well as zero or more arguments to provide
/// to the async function when first executing it
///
/// ```lua
/// local f = wrap_async(some_async_fn, schedule_fn)
/// f(arg1, arg2, ..., function(success, res) end)
/// ```
pub fn wrap_async<'a>(
lua: &'a Lua,
async_fn: LuaFunction<'a>,
schedule_fn: LuaFunction<'a>,
) -> LuaResult<LuaFunction<'a>> {
let pending = pending(lua)?;
lua.load(chunk! {
return function(...)
local args = {...}
local cb = table.remove(args)
assert(type(cb) == "function", "Invalid type for cb")
local schedule = function(...) return $schedule_fn(...) end
// Wrap the async function in a coroutine so we can poll it
local thread = coroutine.create(function(...) return $async_fn(...) end)
// Start the future by peforming the first poll
local status, res = coroutine.resume(thread, unpack(args))
local inner_fn
inner_fn = function()
// Thread has exited already, so res is an error
if not status then
cb(false, res)
// Got pending status on success, so we are still waiting
elseif res == $pending then
// Resume the coroutine and then schedule a followup
// once it has completed another round
status, res = coroutine.resume(thread)
schedule(inner_fn)
// Got success with non-pending status, so this should be the result
else
cb(true, res)
end
end
schedule(inner_fn)
end
})
.eval()
}
/// Return mlua's internal `Poll::Pending`
pub(super) fn pending(lua: &Lua) -> LuaResult<LuaValue> {
let pending = lua.create_async_function(|_, ()| async move {
tokio::task::yield_now().await;
Ok(())
})?;
// Should return mlua's internal Poll::Pending that is statically available
// See https://github.com/khvzak/mlua/issues/76#issuecomment-932645078
lua.load(chunk! {
(coroutine.wrap($pending))()
})
.eval()
}
/// Return a random u32
pub fn rand_u32() -> LuaResult<u32> {
static RAND: OnceCell<Mutex<Rand32>> = OnceCell::new();
Ok(RAND
.get_or_try_init::<_, SystemTimeError>(|| {
Ok(Mutex::new(Rand32::new(
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
)))
})
.to_lua_err()?
.lock()
.map_err(|x| x.to_string())
.to_lua_err()?
.rand_u32())
}

@ -17,6 +17,7 @@ futures = "0.3.16"
log = "0.4.14"
rand = { version = "0.8.4", features = ["getrandom"] }
rpassword = "5.0.1"
shell-words = "1.0"
smol = "1.2"
tokio = { version = "1.12.0", features = ["full"] }
wezterm-ssh = { version = "0.2.0", features = ["vendored-openssl"], git = "https://github.com/chipsenkbeil/wezterm" }

@ -9,7 +9,7 @@ use std::{
collections::HashMap,
future::Future,
io::{self, Read, Write},
path::{Component, Path, PathBuf},
path::{Component, PathBuf},
pin::Pin,
sync::Arc,
};
@ -215,7 +215,7 @@ async fn file_append(
use smol::io::AsyncWriteExt;
let mut file = session
.sftp()
.open_mode(
.open_with_mode(
path,
OpenOptions {
read: false,
@ -246,7 +246,12 @@ async fn dir_read(
let sftp = session.sftp();
// Canonicalize our provided path to ensure that it is exists, not a loop, and absolute
let root_path = sftp.realpath(path).compat().await.map_err(to_other_error)?;
let root_path = sftp
.canonicalize(path)
.compat()
.await
.map_err(to_other_error)?
.into_std_path_buf();
// Build up our entry list
let mut entries = Vec::new();
@ -277,8 +282,8 @@ async fn dir_read(
let is_dir = match ft {
FileType::Dir => true,
FileType::File => false,
FileType::Symlink => match sftp.stat(&path).await {
Ok(stat) => stat.is_dir(),
FileType::Symlink => match sftp.metadata(path.to_path_buf()).await {
Ok(metadata) => metadata.is_dir(),
Err(x) => {
errors.push(DistantError::from(to_other_error(x)));
continue;
@ -288,13 +293,18 @@ async fn dir_read(
// Determine if we continue traversing or stop
if is_dir && (depth == 0 || next_depth <= depth) {
match sftp.readdir(&path).compat().await.map_err(to_other_error) {
match sftp
.read_dir(path.to_path_buf())
.compat()
.await
.map_err(to_other_error)
{
Ok(entries) => {
for (mut path, stat) in entries {
for (mut path, metadata) in entries {
// Canonicalize the path if specified, otherwise just return
// the path as is
path = if canonicalize {
match sftp.realpath(path).compat().await {
match sftp.canonicalize(path).compat().await {
Ok(path) => path,
Err(x) => {
errors.push(DistantError::from(to_other_error(x)));
@ -313,13 +323,13 @@ async fn dir_read(
// the path if the strip_prefix fails
path = path
.strip_prefix(root_path.as_path())
.map(Path::to_path_buf)
.map(|p| p.to_path_buf())
.unwrap_or(path);
};
let ft = stat.ty;
let ft = metadata.ty;
to_traverse.push(DirEntry {
path,
path: path.into_std_path_buf(),
file_type: if ft.is_dir() {
FileType::Dir
} else if ft.is_file() {
@ -350,7 +360,7 @@ async fn dir_create(session: WezSession, path: PathBuf, all: bool) -> io::Result
async fn mkdir(sftp: &wezterm_ssh::Sftp, path: PathBuf) -> io::Result<()> {
// Using 755 as this mirrors "ssh <host> mkdir ..."
// 755: rwxr-xr-x
sftp.mkdir(path, 0o755)
sftp.create_dir(path, 0o755)
.compat()
.await
.map_err(to_other_error)
@ -391,20 +401,20 @@ async fn remove(session: WezSession, path: PathBuf, force: bool) -> io::Result<O
// Determine if we are dealing with a file or directory
let stat = sftp
.stat(path.to_path_buf())
.metadata(path.to_path_buf())
.compat()
.await
.map_err(to_other_error)?;
// If a file or symlink, we just unlink (easy)
if stat.is_file() || stat.is_symlink() {
sftp.unlink(path)
sftp.remove_file(path)
.compat()
.await
.map_err(|x| io::Error::new(io::ErrorKind::PermissionDenied, x))?;
// If directory and not forcing, we just rmdir (easy)
} else if !force {
sftp.rmdir(path)
sftp.remove_dir(path)
.compat()
.await
.map_err(|x| io::Error::new(io::ErrorKind::PermissionDenied, x))?;
@ -426,9 +436,9 @@ async fn remove(session: WezSession, path: PathBuf, force: bool) -> io::Result<O
entries.push(entry);
for (path, stat) in sftp.readdir(path).await.map_err(to_other_error)? {
for (path, stat) in sftp.read_dir(path).await.map_err(to_other_error)? {
to_traverse.push(DirEntry {
path,
path: path.into_std_path_buf(),
file_type: if stat.is_dir() {
FileType::Dir
} else if stat.is_file() {
@ -450,12 +460,12 @@ async fn remove(session: WezSession, path: PathBuf, force: bool) -> io::Result<O
while let Some(entry) = entries.pop() {
if entry.file_type == FileType::Dir {
sftp.rmdir(entry.path)
sftp.remove_dir(entry.path)
.compat()
.await
.map_err(|x| io::Error::new(io::ErrorKind::PermissionDenied, x))?;
} else {
sftp.unlink(entry.path)
sftp.remove_file(entry.path)
.compat()
.await
.map_err(|x| io::Error::new(io::ErrorKind::PermissionDenied, x))?;
@ -521,7 +531,7 @@ async fn exists(session: WezSession, path: PathBuf) -> io::Result<Outgoing> {
// NOTE: SFTP does not provide a means to check if a path exists that can be performed
// separately from getting permission errors; so, we just assume any error means that the path
// does not exist
let exists = session.sftp().lstat(path).compat().await.is_ok();
let exists = session.sftp().symlink_metadata(path).compat().await.is_ok();
Ok(Outgoing::from(ResponseData::Exists(exists)))
}
@ -535,24 +545,28 @@ async fn metadata(
let sftp = session.sftp();
let canonicalized_path = if canonicalize {
Some(
sftp.realpath(path.to_path_buf())
sftp.canonicalize(path.to_path_buf())
.compat()
.await
.map_err(to_other_error)?,
.map_err(to_other_error)?
.into_std_path_buf(),
)
} else {
None
};
let stat = if resolve_file_type {
sftp.stat(path).compat().await.map_err(to_other_error)?
let metadata = if resolve_file_type {
sftp.metadata(path).compat().await.map_err(to_other_error)?
} else {
sftp.lstat(path).compat().await.map_err(to_other_error)?
sftp.symlink_metadata(path)
.compat()
.await
.map_err(to_other_error)?
};
let file_type = if stat.is_dir() {
let file_type = if metadata.is_dir() {
FileType::Dir
} else if stat.is_file() {
} else if metadata.is_file() {
FileType::File
} else {
FileType::Symlink
@ -561,11 +575,11 @@ async fn metadata(
Ok(Outgoing::from(ResponseData::Metadata(Metadata {
canonicalized_path,
file_type,
len: stat.len(),
len: metadata.len(),
// Check that owner, group, or other has write permission (if not, then readonly)
readonly: stat.is_readonly(),
accessed: stat.accessed.map(u128::from),
modified: stat.modified.map(u128::from),
readonly: metadata.is_readonly(),
accessed: metadata.accessed.map(u128::from),
modified: metadata.modified.map(u128::from),
created: None,
})))
}
@ -852,10 +866,11 @@ async fn proc_list(_session: WezSession, state: Arc<Mutex<State>>) -> io::Result
async fn system_info(session: WezSession) -> io::Result<Outgoing> {
let current_dir = session
.sftp()
.realpath(".")
.canonicalize(".")
.compat()
.await
.map_err(to_other_error)?;
.map_err(to_other_error)?
.into_std_path_buf();
let first_component = current_dir.components().next();
let is_windows =

@ -1,5 +1,7 @@
use async_compat::CompatExt;
use distant_core::{Request, Session, Transport};
use distant_core::{
Request, Session, SessionChannelExt, SessionInfo, Transport, XChaCha20Poly1305Codec,
};
use log::*;
use smol::channel::Receiver as SmolReceiver;
use std::{
@ -7,12 +9,14 @@ use std::{
io::{self, Write},
path::PathBuf,
sync::Arc,
time::Duration,
};
use tokio::sync::{mpsc, Mutex};
use wezterm_ssh::{Config as WezConfig, Session as WezSession, SessionEvent as WezSessionEvent};
mod handler;
/// Represents a singular authentication prompt for a new ssh session
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ssh2AuthPrompt {
@ -24,6 +28,8 @@ pub struct Ssh2AuthPrompt {
pub echo: bool,
}
/// Represents an authentication request that needs to be handled before an ssh session can be
/// established
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ssh2AuthEvent {
@ -37,6 +43,7 @@ pub struct Ssh2AuthEvent {
pub prompts: Vec<Ssh2AuthPrompt>,
}
/// Represents options to be provided when establishing an ssh session
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ssh2SessionOpts {
@ -74,10 +81,46 @@ pub struct Ssh2SessionOpts {
pub other: BTreeMap<String, String>,
}
/// Represents options to be provided when converting an ssh session into a distant session
#[derive(Clone, Debug)]
pub struct IntoDistantSessionOpts {
/// Binary to use for distant server
pub binary: String,
/// Arguments to supply to the distant server when starting it
pub args: String,
/// Timeout to use when connecting to the distant server
pub timeout: Duration,
}
impl Default for IntoDistantSessionOpts {
fn default() -> Self {
Self {
binary: String::from("distant"),
args: String::new(),
timeout: Duration::from_secs(15),
}
}
}
/// Represents callback functions to be invoked during authentication of an ssh session
pub struct Ssh2AuthHandler<'a> {
/// Invoked whenever a series of authentication prompts need to be displayed and responded to,
/// receiving one event at a time and returning a collection of answers matching the total
/// prompts provided in the event
pub on_authenticate: Box<dyn FnMut(Ssh2AuthEvent) -> io::Result<Vec<String>> + 'a>,
/// Invoked when receiving a banner from the ssh server, receiving the banner as a str, useful
/// to display to the user
pub on_banner: Box<dyn FnMut(&str) + 'a>,
/// Invoked when the host is unknown for a new ssh connection, receiving the host as a str and
/// returning true if the host is acceptable or false if the host (and thereby ssh session)
/// should be declined
pub on_host_verify: Box<dyn FnMut(&str) -> io::Result<bool> + 'a>,
/// Invoked when an error is encountered, receiving the error as a str
pub on_error: Box<dyn FnMut(&str) + 'a>,
}
@ -134,9 +177,11 @@ impl Default for Ssh2AuthHandler<'static> {
}
}
/// Represents an ssh2 session
pub struct Ssh2Session {
session: WezSession,
events: SmolReceiver<WezSessionEvent>,
authenticated: bool,
}
impl Ssh2Session {
@ -196,11 +241,25 @@ impl Ssh2Session {
let (session, events) =
WezSession::connect(config).map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
Ok(Self { session, events })
Ok(Self {
session,
events,
authenticated: false,
})
}
/// Authenticates the [`Ssh2Session`] and produces a [`Session`]
pub async fn authenticate(self, mut handler: Ssh2AuthHandler<'_>) -> io::Result<Session> {
#[inline]
pub fn is_authenticated(&self) -> bool {
self.authenticated
}
/// Authenticates the [`Ssh2Session`] if not already authenticated
pub async fn authenticate(&mut self, mut handler: Ssh2AuthHandler<'_>) -> io::Result<()> {
// If already authenticated, exit
if self.authenticated {
return Ok(());
}
// Perform the authentication by listening for events and continuing to handle them
// until authenticated
while let Ok(event) = self.events.recv().await {
@ -246,12 +305,92 @@ impl Ssh2Session {
}
}
// We are now authenticated, so convert into a distant session that wraps our ssh2 session
self.into_session()
// Mark as authenticated
self.authenticated = true;
Ok(())
}
/// Consume [`Ssh2Session`] and produce a distant [`Session`] that is connected to a remote
/// distant server that is spawned using the ssh session
pub async fn into_distant_session(self, opts: IntoDistantSessionOpts) -> io::Result<Session> {
// Exit early if not authenticated as this is a requirement
if !self.authenticated {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Not authenticated",
));
}
let mut session = self.into_ssh_client_session()?;
// Build arguments for distant
let mut args = vec![String::from("listen"), String::from("--daemon")];
args.extend(
shell_words::split(&opts.args)
.map_err(|x| io::Error::new(io::ErrorKind::InvalidInput, x))?,
);
// Spawn distant server
let mut proc = session
.spawn("<ssh-launch>", opts.binary, args)
.await
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
let mut stdout = proc.stdout.take().unwrap();
let (success, code) = proc
.wait()
.await
.map_err(|x| io::Error::new(io::ErrorKind::BrokenPipe, x))?;
// Close out ssh session
session.abort();
let _ = session.wait().await;
// If successful, grab the session information and establish a connection
// with the distant server
if success {
let mut out = String::new();
while let Ok(data) = stdout.read().await {
out.push_str(&data);
}
let maybe_info = out
.lines()
.find_map(|line| line.parse::<SessionInfo>().ok());
match maybe_info {
Some(info) => {
let addr = info.to_socket_addr().await?;
let key = info.key;
let codec = XChaCha20Poly1305Codec::from(key);
Session::tcp_connect_timeout(addr, codec, opts.timeout).await
}
None => Err(io::Error::new(
io::ErrorKind::InvalidData,
"Missing session data",
)),
}
} else {
Err(io::Error::new(
io::ErrorKind::Other,
format!(
"Spawning distant failed: {}",
code.map(|x| x.to_string())
.unwrap_or_else(|| String::from("???"))
),
))
}
}
/// Consume [`Ssh2Session`] and produce a distant [`Session`]
fn into_session(self) -> io::Result<Session> {
/// Consume [`Ssh2Session`] and produce a distant [`Session`] that is powered by an ssh client
/// underneath
pub fn into_ssh_client_session(self) -> io::Result<Session> {
// Exit early if not authenticated as this is a requirement
if !self.authenticated {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Not authenticated",
));
}
let (t1, t2) = Transport::pair(1);
let session = Session::initialize(t1)?;

@ -404,7 +404,7 @@ pub fn sshd() -> &'static Sshd {
pub async fn session(sshd: &'_ Sshd, _logger: &'_ flexi_logger::LoggerHandle) -> Session {
let port = sshd.port;
Ssh2Session::connect(
let mut ssh2_session = Ssh2Session::connect(
"127.0.0.1",
Ssh2SessionOpts {
port: Some(port),
@ -415,18 +415,22 @@ pub async fn session(sshd: &'_ Sshd, _logger: &'_ flexi_logger::LoggerHandle) ->
..Default::default()
},
)
.unwrap()
.authenticate(Ssh2AuthHandler {
on_authenticate: Box::new(|ev| {
println!("on_authenticate: {:?}", ev);
Ok(vec![String::new(); ev.prompts.len()])
}),
on_host_verify: Box::new(|host| {
println!("on_host_verify: {}", host);
Ok(true)
}),
..Default::default()
})
.await
.unwrap()
.unwrap();
ssh2_session
.authenticate(Ssh2AuthHandler {
on_authenticate: Box::new(|ev| {
println!("on_authenticate: {:?}", ev);
Ok(vec![String::new(); ev.prompts.len()])
}),
on_host_verify: Box::new(|host| {
println!("on_host_verify: {}", host);
Ok(true)
}),
..Default::default()
})
.await
.unwrap();
ssh2_session.into_ssh_client_session().unwrap()
}

@ -29,7 +29,7 @@ popd () {
}
###############################################################################
# TARGET GENERATION
# TARGET GENERATION FOR DISTANT BIN
#
# Note: This is running on an M1 Mac and expects tooling like `lipo`
###############################################################################
@ -42,13 +42,13 @@ mkdir -p "${PACKAGE_DIR}"
# Apple x86-64 on M1 Mac
TARGET="x86_64-apple-darwin"
echo "Building ${TARGET}"
echo "Building ${TARGET} distant binary"
cargo build --release --target "${TARGET}"
strip "${TARGET_DIR}/${TARGET}/release/distant"
# Apple ARM on M1 Mac
TARGET="aarch64-apple-darwin"
echo "Building ${TARGET}"
echo "Building ${TARGET} distant binary"
cargo build --release --target "${TARGET}"
strip "${TARGET_DIR}/${TARGET}/release/distant"
@ -61,14 +61,14 @@ lipo -create \
# Linux x86-64 (libc) on M1 Mac
TARGET="x86_64-unknown-linux-gnu"
echo "Building ${TARGET}"
echo "Building ${TARGET} distant binary"
cargo build --release --target "${TARGET}"
cp "${TARGET_DIR}/${TARGET}/release/distant" "${PACKAGE_DIR}/distant-linux64-gnu"
x86_64-unknown-linux-musl-strip "${PACKAGE_DIR}/distant-linux64-gnu"
# Linux x86-64 (musl) on M1 Mac
TARGET="x86_64-unknown-linux-musl"
echo "Building ${TARGET}"
echo "Building ${TARGET} distant binary"
cargo build --release --target "${TARGET}"
cp "${TARGET_DIR}/${TARGET}/release/distant" "${PACKAGE_DIR}/distant-linux64-musl"
x86_64-unknown-linux-musl-strip "${PACKAGE_DIR}/distant-linux64-musl"
@ -84,6 +84,31 @@ for bin in *; do
done
popd
###############################################################################
# TARGET GENERATION FOR DISTANT LUA MODULE
###############################################################################
# Apple x86-64 on M1 Mac
TARGETS=(
"x86_64-apple-darwin"
"aarch64-apple-darwin"
"x86_64-unknown-linux-gnu"
)
pushd "distant-lua";
for TARGET in "${TARGETS[@]}"; do
echo "Building ${TARGET} for Lua"
cargo build --release --target "${TARGET}"
if [ "$TARGET" == "x86_64-apple-darwin" ]; then
cp "${TARGET_DIR}/${TARGET}/release/libdistant_lua.dylib" "${PACKAGE_DIR}/distant_lua-macos-x86_64.so"
elif [ "$TARGET" == "aarch64-apple-darwin" ]; then
cp "${TARGET_DIR}/${TARGET}/release/libdistant_lua.dylib" "${PACKAGE_DIR}/distant_lua-macos-aarch64.so"
else
cp "${TARGET_DIR}/${TARGET}/release/libdistant_lua.so" "${PACKAGE_DIR}/distant_lua-linux-x86_64.so"
fi
done
popd
###############################################################################
# SHA 256 GENERATION
#

@ -6,6 +6,7 @@
CRATES=(
distant-core
distant-ssh2
distant-lua
distant
)

@ -21,7 +21,7 @@ use strum::{EnumString, EnumVariantNames, IntoStaticStr, VariantNames};
static USERNAME: Lazy<String> = Lazy::new(whoami::username);
/// Options and commands to apply to binary
#[derive(Debug, StructOpt)]
#[derive(Clone, Debug, StructOpt)]
#[structopt(name = "distant")]
pub struct Opt {
#[structopt(flatten)]
@ -74,7 +74,7 @@ impl LogLevel {
}
/// Contains options that are common across subcommands
#[derive(Debug, StructOpt)]
#[derive(Clone, Debug, StructOpt)]
pub struct CommonOpt {
/// Quiet mode, suppresses all logging (shortcut for log level off)
#[structopt(short, long, global = true)]
@ -108,7 +108,7 @@ impl CommonOpt {
}
/// Contains options related sessions
#[derive(Debug, StructOpt)]
#[derive(Clone, Debug, StructOpt)]
pub struct SessionOpt {
/// Represents the location of the file containing session information,
/// only useful when the session is set to "file"
@ -137,7 +137,7 @@ pub struct SshConnectionOpts {
pub user: Option<String>,
}
#[derive(Debug, StructOpt)]
#[derive(Clone, Debug, StructOpt)]
pub enum Subcommand {
/// Performs some action on a remote machine
Action(ActionSubcommand),
@ -232,7 +232,7 @@ pub enum Format {
}
/// Represents subcommand to execute some operation remotely
#[derive(Debug, StructOpt)]
#[derive(Clone, Debug, StructOpt)]
#[structopt(verbatim_doc_comment)]
pub struct ActionSubcommand {
/// Represents the format that results should be returned
@ -441,7 +441,7 @@ impl Default for SessionInput {
}
/// Represents subcommand to launch a remote server
#[derive(Debug, StructOpt)]
#[derive(Clone, Debug, StructOpt)]
pub struct LaunchSubcommand {
/// Represents the medium for sharing the session upon launching on a remote machine
#[structopt(
@ -551,7 +551,7 @@ impl LaunchSubcommand {
}
/// Represents subcommand to operate in listen mode for incoming requests
#[derive(Debug, StructOpt)]
#[derive(Clone, Debug, StructOpt)]
pub struct ListenSubcommand {
/// Runs in background via daemon-mode (does nothing on windows)
#[structopt(short, long)]
@ -613,7 +613,7 @@ impl ListenSubcommand {
}
/// Represents subcommand to execute some LSP server on a remote machine
#[derive(Debug, StructOpt)]
#[derive(Clone, Debug, StructOpt)]
#[structopt(verbatim_doc_comment)]
pub struct LspSubcommand {
/// Represents the format that results should be returned

@ -9,7 +9,6 @@ use distant_core::{
PlainCodec, RelayServer, Session, SessionInfo, SessionInfoFile, Transport, TransportListener,
XChaCha20Poly1305Codec,
};
use fork::{daemon, Fork};
use log::*;
use std::{path::Path, string::FromUtf8Error};
use tokio::{io, process::Command, runtime::Runtime, time::Duration};
@ -63,6 +62,7 @@ pub fn run(cmd: LaunchSubcommand, opt: CommonOpt) -> Result<(), Error> {
debug!("Piping session to stdout");
println!("{}", session.to_unprotected_string())
}
#[cfg(unix)]
SessionOutput::Socket if is_daemon => {
debug!(
"Forking and entering interactive loop over unix socket {:?}",
@ -73,26 +73,13 @@ pub fn run(cmd: LaunchSubcommand, opt: CommonOpt) -> Result<(), Error> {
// this produces a garbage process that won't die
drop(rt);
match daemon(false, false) {
Ok(Fork::Child) => {
// NOTE: We need to create a runtime within the forked process as
// tokio's runtime doesn't support being transferred from
// parent to child in a fork
let rt = Runtime::new()?;
rt.block_on(async {
socket_loop(
session_socket,
session,
timeout,
fail_if_socket_exists,
shutdown_after,
)
.await
})?
}
Ok(_) => {}
Err(x) => return Err(Error::Fork(x)),
}
run_daemon_socket(
session_socket,
session,
timeout,
fail_if_socket_exists,
shutdown_after,
)?;
}
#[cfg(unix)]
SessionOutput::Socket => {
@ -111,14 +98,39 @@ pub fn run(cmd: LaunchSubcommand, opt: CommonOpt) -> Result<(), Error> {
.await
})?
}
#[cfg(not(unix))]
SessionOutput::Socket => {
debug!(concat!(
"Trying to enter interactive loop over unix socket, ",
"but not on unix platform!"
));
unreachable!()
}
Ok(())
}
#[cfg(unix)]
fn run_daemon_socket(
session_socket: impl AsRef<Path>,
session: SessionInfo,
timeout: Duration,
fail_if_socket_exists: bool,
shutdown_after: Option<Duration>,
) -> Result<(), Error> {
use fork::{daemon, Fork};
match daemon(false, false) {
Ok(Fork::Child) => {
// NOTE: We need to create a runtime within the forked process as
// tokio's runtime doesn't support being transferred from
// parent to child in a fork
let rt = Runtime::new()?;
rt.block_on(async {
socket_loop(
session_socket,
session,
timeout,
fail_if_socket_exists,
shutdown_after,
)
.await
})?
}
Ok(_) => {}
Err(x) => return Err(Error::Fork(x)),
}
Ok(())
@ -136,6 +148,7 @@ async fn keep_loop(info: SessionInfo, format: Format, duration: Duration) -> io:
}
}
#[cfg(unix)]
async fn socket_loop(
socket_path: impl AsRef<Path>,
info: SessionInfo,

@ -6,7 +6,6 @@ use derive_more::{Display, Error, From};
use distant_core::{
DistantServer, DistantServerOptions, SecretKey32, UnprotectedToHexKey, XChaCha20Poly1305Codec,
};
use fork::{daemon, Fork};
use log::*;
use tokio::{io, task::JoinError};
@ -31,20 +30,7 @@ impl ExitCodeError for Error {
pub fn run(cmd: ListenSubcommand, opt: CommonOpt) -> Result<(), Error> {
if cmd.daemon {
// NOTE: We keep the stdin, stdout, stderr open so we can print out the pid with the parent
match daemon(false, true) {
Ok(Fork::Child) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { run_async(cmd, opt, true).await })?;
}
Ok(Fork::Parent(pid)) => {
info!("[distant detached, pid = {}]", pid);
if fork::close_fd().is_err() {
return Err(Error::Fork);
}
}
Err(_) => return Err(Error::Fork),
}
run_daemon(cmd, opt)?;
} else {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { run_async(cmd, opt, false).await })?;
@ -53,6 +39,45 @@ pub fn run(cmd: ListenSubcommand, opt: CommonOpt) -> Result<(), Error> {
Ok(())
}
#[cfg(windows)]
fn run_daemon(_cmd: ListenSubcommand, _opt: CommonOpt) -> Result<(), Error> {
use std::process::{Command, Stdio};
let mut args = std::env::args_os().filter(|arg| arg != "--daemon");
let program = args.next().ok_or(Error::Fork)?;
let child = Command::new(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
info!("[distant detached, pid = {}]", child.id());
Ok(())
}
#[cfg(unix)]
fn run_daemon(cmd: ListenSubcommand, opt: CommonOpt) -> Result<(), Error> {
use fork::{daemon, Fork};
// NOTE: We keep the stdin, stdout, stderr open so we can print out the pid with the parent
match daemon(false, true) {
Ok(Fork::Child) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { run_async(cmd, opt, true).await })?;
Ok(())
}
Ok(Fork::Parent(pid)) => {
info!("[distant detached, pid = {}]", pid);
if fork::close_fd().is_err() {
Err(Error::Fork)
} else {
Ok(())
}
}
Err(_) => Err(Error::Fork),
}
}
async fn run_async(cmd: ListenSubcommand, _opt: CommonOpt, is_forked: bool) -> Result<(), Error> {
let addr = cmd.host.to_ip_addr(cmd.use_ipv6)?;
let shutdown_after = cmd.to_shutdown_after_duration();
@ -83,6 +108,7 @@ async fn run_async(cmd: ListenSubcommand, _opt: CommonOpt, is_forked: bool) -> R
println!("DISTANT DATA -- {} {}", port, key_hex_string);
// For the child, we want to fully disconnect it from pipes, which we do now
#[cfg(unix)]
if is_forked && fork::close_fd().is_err() {
return Err(Error::Fork);
}

@ -51,7 +51,7 @@ impl CommandRunner {
use distant_ssh2::{Ssh2Session, Ssh2SessionOpts};
let SshConnectionOpts { host, port, user } = ssh_connection;
let session = Ssh2Session::connect(
let mut session = Ssh2Session::connect(
host,
Ssh2SessionOpts {
port: Some(port),
@ -59,12 +59,14 @@ impl CommandRunner {
..Default::default()
},
)
.map_err(wrap_err)?
.authenticate(Default::default())
.await
.map_err(wrap_err)?;
(session, None)
session
.authenticate(Default::default())
.await
.map_err(wrap_err)?;
(session.into_ssh_client_session().map_err(wrap_err)?, None)
}
Method::Distant => {
@ -82,6 +84,7 @@ impl CommandRunner {
.map_err(wrap_err)?;
(session, lsp_data)
}
#[cfg(unix)]
SessionParams::Socket { path, codec } => {
let session = Session::unix_connect_timeout(path, codec, timeout)
.await
@ -102,10 +105,8 @@ enum SessionParams {
codec: XChaCha20Poly1305Codec,
lsp_data: Option<LspData>,
},
Socket {
path: PathBuf,
codec: PlainCodec,
},
#[cfg(unix)]
Socket { path: PathBuf, codec: PlainCodec },
}
async fn retrieve_session_params(

Loading…
Cancel
Save