Add filesystem watching & remove distant-lua (#102)

pull/104/head
Chip Senkbeil 2 years ago committed by GitHub
parent f46eeea8d5
commit 268ec948d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -29,12 +29,6 @@ jobs:
run: cargo clippy -p distant-core --all-targets --verbose --all-features run: cargo clippy -p distant-core --all-targets --verbose --all-features
- name: distant-ssh2 (all features) - name: distant-ssh2 (all features)
run: cargo clippy -p distant-ssh2 --all-targets --verbose --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) - name: distant (all features)
run: cargo clippy --all-targets --verbose --all-features run: cargo clippy --all-targets --verbose --all-features

@ -44,6 +44,3 @@ jobs:
- name: Run CLI tests (no default features) - name: Run CLI tests (no default features)
run: cargo test --verbose --no-default-features run: cargo test --verbose --no-default-features
shell: bash shell: bash
- name: Run Lua tests
run: (cd distant-lua && cargo build) && (cd distant-lua-tests && cargo test --verbose)
shell: bash

@ -41,6 +41,3 @@ jobs:
- name: Run CLI tests (no default features) - name: Run CLI tests (no default features)
run: cargo test --verbose --no-default-features run: cargo test --verbose --no-default-features
shell: bash shell: bash
- name: Run Lua tests
run: (cd distant-lua && cargo build) && (cd distant-lua-tests && cargo test --verbose)
shell: bash

@ -43,15 +43,3 @@ jobs:
- name: Build CLI (no default features) - name: Build CLI (no default features)
run: cargo build --verbose --no-default-features run: cargo build --verbose --no-default-features
shell: bash shell: bash
- uses: xpol/setup-lua@v0.3
with:
lua-version: "5.1.5"
- 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

@ -6,10 +6,6 @@ on:
- v[0-9]+.[0-9]+.[0-9]+ - v[0-9]+.[0-9]+.[0-9]+
- v[0-9]+.[0-9]+.[0-9]+-** - v[0-9]+.[0-9]+.[0-9]+-**
env:
LUA_VERSION: 5.1.5
LUA_FEATURE: lua51
jobs: jobs:
macos: macos:
name: "Build release on MacOS" name: "Build release on MacOS"
@ -22,11 +18,7 @@ jobs:
X86_DIR: target/x86_64-apple-darwin/release X86_DIR: target/x86_64-apple-darwin/release
ARM_DIR: target/aarch64-apple-darwin/release ARM_DIR: target/aarch64-apple-darwin/release
BUILD_BIN: distant BUILD_BIN: distant
BUILD_LIB: libdistant_lua.dylib
UNIVERSAL_REL_BIN: distant-macos UNIVERSAL_REL_BIN: distant-macos
UNIVERSAL_REL_LIB: distant_lua-macos.dylib
X86_REL_LIB: distant_lua-macos-intel.dylib
ARM_REL_LIB: distant_lua-macos-arm.dylib
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install Rust (x86) - name: Install Rust (x86)
@ -42,24 +34,6 @@ jobs:
toolchain: stable toolchain: stable
target: ${{ env.ARM_ARCH }} target: ${{ env.ARM_ARCH }}
- uses: Swatinem/rust-cache@v1 - uses: Swatinem/rust-cache@v1
- name: Build Lua ${{ env.LUA_VERSION }} library (x86_64)
run: |
cd distant-lua
cargo build --release --no-default-features --features "${{ env.LUA_FEATURE }},vendored" --target ${{ env.X86_ARCH }}
ls -l ../${{ env.X86_DIR }}
cp ../${{ env.X86_DIR }}/${{ env.BUILD_LIB }} ../${{ env.X86_REL_LIB }}
- name: Build Lua ${{ env.LUA_VERSION }} library (aarch64)
run: |
cd distant-lua
cargo build --release --no-default-features --features "${{ env.LUA_FEATURE }},vendored" --target ${{ env.ARM_ARCH }}
ls -l ../${{ env.ARM_DIR }}
cp ../${{ env.ARM_DIR }}/${{ env.BUILD_LIB }} ../${{ env.ARM_REL_LIB }}
- name: Unify libraries
run: |
lipo -create -output ${{ env.UNIVERSAL_REL_LIB }} \
./${{ env.X86_DIR }}/${{ env.BUILD_LIB }} \
./${{ env.ARM_DIR }}/${{ env.BUILD_LIB }}
chmod +x ./${{ env.UNIVERSAL_REL_LIB }}
- name: Build binary (x86_64) - name: Build binary (x86_64)
run: | run: |
cargo build --release --all-features --target ${{ env.X86_ARCH }} cargo build --release --all-features --target ${{ env.X86_ARCH }}
@ -82,9 +56,6 @@ jobs:
name: ${{ env.UPLOAD_NAME }} name: ${{ env.UPLOAD_NAME }}
path: | path: |
${{ env.UNIVERSAL_REL_BIN }} ${{ env.UNIVERSAL_REL_BIN }}
${{ env.UNIVERSAL_REL_LIB }}
${{ env.X86_REL_LIB }}
${{ env.ARM_REL_LIB }}
windows: windows:
name: "Build release on Windows" name: "Build release on Windows"
@ -95,9 +66,7 @@ jobs:
X86_ARCH: x86_64-pc-windows-msvc X86_ARCH: x86_64-pc-windows-msvc
X86_DIR: target/x86_64-pc-windows-msvc/release X86_DIR: target/x86_64-pc-windows-msvc/release
BUILD_BIN: distant.exe BUILD_BIN: distant.exe
BUILD_LIB: distant_lua.dll
X86_REL_BIN: distant-win64.exe X86_REL_BIN: distant-win64.exe
X86_REL_LIB: distant_lua-win64.dll
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install Rust (MSVC) - name: Install Rust (MSVC)
@ -107,19 +76,6 @@ jobs:
toolchain: stable toolchain: stable
target: ${{ env.X86_ARCH }} target: ${{ env.X86_ARCH }}
- uses: Swatinem/rust-cache@v1 - uses: Swatinem/rust-cache@v1
- uses: xpol/setup-lua@v0.3
with:
lua-version: "${{ env.LUA_VERSION }}"
- name: Build Lua ${{ env.LUA_VERSION }} library (x86_64)
run: |
cd distant-lua
cargo build --release --no-default-features --features "${{ env.LUA_FEATURE }}" --target ${{ env.X86_ARCH }}
ls -l ../${{ env.X86_DIR }}
mv ../${{ env.X86_DIR }}/${{ env.BUILD_LIB }} ../${{ env.X86_REL_LIB }}
env:
LUA_INC: ${{ github.workspace }}\.lua\include
LUA_LIB: ${{ github.workspace }}\.lua\lib
LUA_LIB_NAME: lua
- name: Build binary (x86_64) - name: Build binary (x86_64)
run: | run: |
cargo build --release --all-features --target ${{ env.X86_ARCH }} cargo build --release --all-features --target ${{ env.X86_ARCH }}
@ -132,7 +88,6 @@ jobs:
with: with:
name: ${{ env.UPLOAD_NAME }} name: ${{ env.UPLOAD_NAME }}
path: | path: |
${{ env.X86_REL_LIB }}
${{ env.X86_REL_BIN }} ${{ env.X86_REL_BIN }}
linux_gnu: linux_gnu:
@ -144,9 +99,7 @@ jobs:
X86_GNU_ARCH: x86_64-unknown-linux-gnu X86_GNU_ARCH: x86_64-unknown-linux-gnu
X86_GNU_DIR: target/x86_64-unknown-linux-gnu/release X86_GNU_DIR: target/x86_64-unknown-linux-gnu/release
BUILD_BIN: distant BUILD_BIN: distant
BUILD_LIB: libdistant_lua.so
X86_GNU_REL_BIN: distant-linux64-gnu X86_GNU_REL_BIN: distant-linux64-gnu
X86_GNU_REL_LIB: distant_lua-linux64-gnu.so
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install Rust (GNU) - name: Install Rust (GNU)
@ -156,12 +109,6 @@ jobs:
toolchain: stable toolchain: stable
target: ${{ env.X86_GNU_ARCH }} target: ${{ env.X86_GNU_ARCH }}
- uses: Swatinem/rust-cache@v1 - uses: Swatinem/rust-cache@v1
- name: Build Lua ${{ env.LUA_VERSION }} library (GNU x86_64)
run: |
cd distant-lua
cargo build --release --no-default-features --features "${{ env.LUA_FEATURE }},vendored" --target ${{ env.X86_GNU_ARCH }}
ls -l ../${{ env.X86_GNU_DIR }}
mv ../${{ env.X86_GNU_DIR }}/${{ env.BUILD_LIB }} ../${{ env.X86_GNU_REL_LIB }}
- name: Build binary (GNU x86_64) - name: Build binary (GNU x86_64)
run: | run: |
cargo build --release --all-features --target ${{ env.X86_GNU_ARCH }} cargo build --release --all-features --target ${{ env.X86_GNU_ARCH }}
@ -174,7 +121,6 @@ jobs:
with: with:
name: ${{ env.UPLOAD_NAME }} name: ${{ env.UPLOAD_NAME }}
path: | path: |
${{ env.X86_GNU_REL_LIB }}
${{ env.X86_GNU_REL_BIN }} ${{ env.X86_GNU_REL_BIN }}
linux_musl: linux_musl:
@ -188,9 +134,7 @@ jobs:
X86_MUSL_ARCH: x86_64-unknown-linux-musl X86_MUSL_ARCH: x86_64-unknown-linux-musl
X86_MUSL_DIR: target/x86_64-unknown-linux-musl/release X86_MUSL_DIR: target/x86_64-unknown-linux-musl/release
BUILD_BIN: distant BUILD_BIN: distant
BUILD_LIB: libdistant_lua.so
X86_MUSL_REL_BIN: distant-linux64-musl X86_MUSL_REL_BIN: distant-linux64-musl
X86_MUSL_REL_LIB: distant_lua-linux64-musl.so
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install base dependencies - name: Install base dependencies
@ -200,15 +144,6 @@ jobs:
run: | run: |
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
- uses: Swatinem/rust-cache@v1 - uses: Swatinem/rust-cache@v1
- name: Build Lua ${{ env.LUA_VERSION }} library (MUSL x86_64)
run: |
cd distant-lua
source $HOME/.cargo/env
cargo build --release --no-default-features --features "${{ env.LUA_FEATURE }},vendored" --target ${{ env.X86_MUSL_ARCH }}
ls -l ../${{ env.X86_MUSL_DIR }}
mv ../${{ env.X86_MUSL_DIR }}/${{ env.BUILD_LIB }} ../${{ env.X86_MUSL_REL_LIB }}
env:
RUSTFLAGS: -C target-feature=-crt-static -C linker=x86_64-alpine-linux-musl-gcc
- name: Build binary (MUSL x86_64) - name: Build binary (MUSL x86_64)
run: | run: |
source $HOME/.cargo/env source $HOME/.cargo/env
@ -222,7 +157,6 @@ jobs:
with: with:
name: ${{ env.UPLOAD_NAME }} name: ${{ env.UPLOAD_NAME }}
path: | path: |
${{ env.X86_MUSL_REL_LIB }}
${{ env.X86_MUSL_REL_BIN }} ${{ env.X86_MUSL_REL_BIN }}
publish: publish:
@ -234,50 +168,32 @@ jobs:
env: env:
MACOS: macos MACOS: macos
MACOS_UNIVERSAL_BIN: distant-macos MACOS_UNIVERSAL_BIN: distant-macos
MACOS_UNIVERSAL_LIB: distant_lua-macos.dylib
MACOS_X86_LIB: distant_lua-macos-intel.dylib
MACOS_ARM_LIB: distant_lua-macos-arm.dylib
WIN64: win64 WIN64: win64
WIN64_BIN: distant-win64.exe WIN64_BIN: distant-win64.exe
WIN64_LIB: distant_lua-win64.dll
LINUX64_GNU: linux64-gnu LINUX64_GNU: linux64-gnu
LINUX64_GNU_BIN: distant-linux64-gnu LINUX64_GNU_BIN: distant-linux64-gnu
LINUX64_GNU_LIB: distant_lua-linux64-gnu.so
LINUX64_MUSL: linux64-musl LINUX64_MUSL: linux64-musl
LINUX64_MUSL_BIN: distant-linux64-musl LINUX64_MUSL_BIN: distant-linux64-musl
LINUX64_MUSL_LIB: distant_lua-linux64-musl.so
steps: steps:
- uses: actions/download-artifact@v2 - uses: actions/download-artifact@v2
- name: Generate MacOS SHA256 checksums - name: Generate MacOS SHA256 checksums
run: | run: |
cd ${{ env.MACOS }} cd ${{ env.MACOS }}
sha256sum ${{ env.MACOS_UNIVERSAL_LIB }} > ${{ env.MACOS_UNIVERSAL_LIB }}.sha256sum
echo "SHA_MACOS_LUA_LIB=$(cat ${{ env.MACOS_UNIVERSAL_LIB }}.sha256sum)" >> $GITHUB_ENV
sha256sum ${{ env.MACOS_X86_LIB }} > ${{ env.MACOS_X86_LIB }}.sha256sum
echo "SHA_MACOS_X86_LUA_LIB=$(cat ${{ env.MACOS_X86_LIB }}.sha256sum)" >> $GITHUB_ENV
sha256sum ${{ env.MACOS_ARM_LIB }} > ${{ env.MACOS_ARM_LIB }}.sha256sum
echo "SHA_MACOS_ARM_LUA_LIB=$(cat ${{ env.MACOS_ARM_LIB }}.sha256sum)" >> $GITHUB_ENV
sha256sum ${{ env.MACOS_UNIVERSAL_BIN }} > ${{ env.MACOS_UNIVERSAL_BIN }}.sha256sum sha256sum ${{ env.MACOS_UNIVERSAL_BIN }} > ${{ env.MACOS_UNIVERSAL_BIN }}.sha256sum
echo "SHA_MACOS_BIN=$(cat ${{ env.MACOS_UNIVERSAL_BIN }}.sha256sum)" >> $GITHUB_ENV echo "SHA_MACOS_BIN=$(cat ${{ env.MACOS_UNIVERSAL_BIN }}.sha256sum)" >> $GITHUB_ENV
- name: Generate Win64 SHA256 checksums - name: Generate Win64 SHA256 checksums
run: | run: |
cd ${{ env.WIN64 }} cd ${{ env.WIN64 }}
sha256sum ${{ env.WIN64_LIB }} > ${{ env.WIN64_LIB }}.sha256sum
echo "SHA_WIN64_LUA_LIB=$(cat ${{ env.WIN64_LIB }}.sha256sum)" >> $GITHUB_ENV
sha256sum ${{ env.WIN64_BIN }} > ${{ env.WIN64_BIN }}.sha256sum sha256sum ${{ env.WIN64_BIN }} > ${{ env.WIN64_BIN }}.sha256sum
echo "SHA_WIN64_BIN=$(cat ${{ env.WIN64_BIN }}.sha256sum)" >> $GITHUB_ENV echo "SHA_WIN64_BIN=$(cat ${{ env.WIN64_BIN }}.sha256sum)" >> $GITHUB_ENV
- name: Generate Linux64 (gnu) SHA256 checksums - name: Generate Linux64 (gnu) SHA256 checksums
run: | run: |
cd ${{ env.LINUX64_GNU }} cd ${{ env.LINUX64_GNU }}
sha256sum ${{ env.LINUX64_GNU_LIB }} > ${{ env.LINUX64_GNU_LIB }}.sha256sum
echo "SHA_LINUX64_GNU_LUA_LIB=$(cat ${{ env.LINUX64_GNU_LIB }}.sha256sum)" >> $GITHUB_ENV
sha256sum ${{ env.LINUX64_GNU_BIN }} > ${{ env.LINUX64_GNU_BIN }}.sha256sum sha256sum ${{ env.LINUX64_GNU_BIN }} > ${{ env.LINUX64_GNU_BIN }}.sha256sum
echo "SHA_LINUX64_GNU_BIN=$(cat ${{ env.LINUX64_GNU_BIN }}.sha256sum)" >> $GITHUB_ENV echo "SHA_LINUX64_GNU_BIN=$(cat ${{ env.LINUX64_GNU_BIN }}.sha256sum)" >> $GITHUB_ENV
- name: Generate Linux64 (musl) SHA256 checksums - name: Generate Linux64 (musl) SHA256 checksums
run: | run: |
cd ${{ env.LINUX64_MUSL }} cd ${{ env.LINUX64_MUSL }}
sha256sum ${{ env.LINUX64_MUSL_LIB }} > ${{ env.LINUX64_MUSL_LIB }}.sha256sum
echo "SHA_LINUX64_MUSL_LUA_LIB=$(cat ${{ env.LINUX64_MUSL_LIB }}.sha256sum)" >> $GITHUB_ENV
sha256sum ${{ env.LINUX64_MUSL_BIN }} > ${{ env.LINUX64_MUSL_BIN }}.sha256sum sha256sum ${{ env.LINUX64_MUSL_BIN }} > ${{ env.LINUX64_MUSL_BIN }}.sha256sum
echo "SHA_LINUX64_MUSL_BIN=$(cat ${{ env.LINUX64_MUSL_BIN }}.sha256sum)" >> $GITHUB_ENV echo "SHA_LINUX64_MUSL_BIN=$(cat ${{ env.LINUX64_MUSL_BIN }}.sha256sum)" >> $GITHUB_ENV
- name: Determine git tag - name: Determine git tag
@ -302,47 +218,14 @@ jobs:
target_commitish: ${{ github.sha }} target_commitish: ${{ github.sha }}
draft: false draft: false
prerelease: ${{ steps.check-tag.outputs.match == 'true' }} prerelease: ${{ steps.check-tag.outputs.match == 'true' }}
# NOTE: MacOS universal and aarch64 Lua libs are withheld due to
# https://github.com/khvzak/mlua/issues/82 and must be
# built and added to each release manually
files: | files: |
${{ env.MACOS }}/${{ env.MACOS_UNIVERSAL_BIN }} ${{ env.MACOS }}/${{ env.MACOS_UNIVERSAL_BIN }}
${{ env.MACOS }}/${{ env.MACOS_UNIVERSAL_LIB }}
${{ env.MACOS }}/${{ env.MACOS_X86_LIB }}
${{ env.MACOS }}/${{ env.MACOS_ARM_LIB }}
${{ env.WIN64 }}/${{ env.WIN64_BIN }} ${{ env.WIN64 }}/${{ env.WIN64_BIN }}
${{ env.WIN64 }}/${{ env.WIN64_LIB }}
${{ env.LINUX64_GNU }}/${{ env.LINUX64_GNU_BIN }} ${{ env.LINUX64_GNU }}/${{ env.LINUX64_GNU_BIN }}
${{ env.LINUX64_GNU }}/${{ env.LINUX64_GNU_LIB }}
${{ env.LINUX64_MUSL }}/${{ env.LINUX64_MUSL_BIN }} ${{ env.LINUX64_MUSL }}/${{ env.LINUX64_MUSL_BIN }}
${{ env.LINUX64_MUSL }}/${{ env.LINUX64_MUSL_LIB }}
**/*.sha256sum **/*.sha256sum
body: | body: |
## Install Lua library ## Binaries
### Windows
1. Download **${{ env.WIN64_LIB }}**
2. Rename to `distant_lua.dll`
3. Import via `distant = require("distant_lua")`
### macOS
1. Download **${{ env.MACOS_UNIVERSAL_LIB }}** (or **${{ env.MACOS_X86_LIB }}** or **${{ env.MACOS_ARM_LIB }}**)
2. Rename to `distant_lua.so` (still works on Mac for Lua)
- Alternatively, you can rename to `distant_lua.dylib` and add
`package.cpath = package.cpath .. ";?.dylib"` within your Lua code before
requiring the library
3. Import via `distant = require("distant_lua")`
### Linux
1. Download **${{ env.LINUX64_GNU_LIB }}** (or **${{ env.LINUX64_MUSL_LIB }}**)
2. Rename to `distant_lua.so`
3. Import via `distant = require("distant_lua")`
## Artifacts
A Lua library is built out to provide bindings to `distant-core` and `distant-ssh2` within Lua.
While this is geared towards usage in neovim, this Lua binding is generic and can be used in Lua
anyway. The library is built against Lua ${{ env.LUA_VERSION }}. Make sure to rename the
library to `distant_lua.{dll,dylib,so}` prior to importing as that is the expected name!
- **linux64** is the Linux library that supports the x86-64 platform using libc
- **macos** is the universal MacOS library that supports x86-64 and aarch64 (ARM) platforms
- **win64** is the Windows library release that supports the x86-64 platform and built via MSVC
Standalone binaries are built out for Windows (x86_64), MacOS (Intel & ARM), and Linux (x86_64). Standalone binaries are built out for Windows (x86_64), MacOS (Intel & ARM), and Linux (x86_64).
- **linux64-gnu** is the x86-64 release on Linux using libc - **linux64-gnu** is the x86-64 release on Linux using libc
- **linux64-musl** is the x86-64 release on Linux using musl (static binary, no libc dependency) - **linux64-musl** is the x86-64 release on Linux using musl (static binary, no libc dependency)
@ -350,14 +233,8 @@ jobs:
- **win64** is the x86-64 release on Windows using MSVC - **win64** is the x86-64 release on Windows using MSVC
## SHA256 Checksums ## SHA256 Checksums
``` ```
${{ env.SHA_MACOS_LUA_LIB }}
${{ env.SHA_MACOS_X86_LUA_LIB }}
${{ env.SHA_MACOS_ARM_LUA_LIB }}
${{ env.SHA_MACOS_BIN }} ${{ env.SHA_MACOS_BIN }}
${{ env.SHA_WIN64_LUA_LIB }}
${{ env.SHA_WIN64_BIN }} ${{ env.SHA_WIN64_BIN }}
${{ env.SHA_LINUX64_GNU_LUA_LIB }}
${{ env.SHA_LINUX64_MUSL_LUA_LIB }}
${{ env.SHA_LINUX64_GNU_BIN }} ${{ env.SHA_LINUX64_GNU_BIN }}
${{ env.SHA_LINUX64_MUSL_BIN }} ${{ env.SHA_LINUX64_MUSL_BIN }}
``` ```

@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
command for distant cli command for distant cli
- Support for JSON communication of ssh auth during launch (cli) - Support for JSON communication of ssh auth during launch (cli)
- Add windows and unix metadata files to overall metadata response data - Add windows and unix metadata files to overall metadata response data
- Watch and unwatch cli commands powered by underlying `Watcher` core
implementation that uses new `RequestData::Watch`, `RequestData::Unwatch`,
and `ResponseData::Changed` data types to communicate filesystem changes
### Changed ### Changed
- Default session type for CLI (launch, action, etc) is `environment` - Default session type for CLI (launch, action, etc) is `environment`
@ -29,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed ### Removed
- Github actions no longer use paths-filter so every PR & commit will test - Github actions no longer use paths-filter so every PR & commit will test
everything everything
- `distant-lua` and `distant-lua-test` no longer exist as we are focusing
solely on the JSON API for integration into distant
## [0.15.1] - 2021-11-15 ## [0.15.1] - 2021-11-15
### Added ### Added

245
Cargo.lock generated

@ -579,6 +579,8 @@ dependencies = [
"hex", "hex",
"indoc", "indoc",
"log", "log",
"normpath",
"notify",
"once_cell", "once_cell",
"portable-pty", "portable-pty",
"predicates", "predicates",
@ -592,40 +594,6 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "distant-lua"
version = "0.16.0"
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]] [[package]]
name = "distant-ssh2" name = "distant-ssh2"
version = "0.16.0" version = "0.16.0"
@ -663,15 +631,6 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 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]] [[package]]
name = "event-listener" name = "event-listener"
version = "2.5.1" version = "2.5.1"
@ -710,6 +669,18 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "filetime"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"winapi",
]
[[package]] [[package]]
name = "flexi_logger" name = "flexi_logger"
version = "0.18.1" version = "0.18.1"
@ -767,6 +738,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.17" version = "0.3.17"
@ -995,6 +975,26 @@ dependencies = [
"unindent", "unindent",
] ]
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.11" version = "0.1.11"
@ -1037,6 +1037,26 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "kqueue"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058a107a784f8be94c7d35c1300f4facced2e93d2fbe5b1452b44e905ddca4a9"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587"
dependencies = [
"bitflags",
"libc",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -1116,24 +1136,6 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
[[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.3.2+resty1085a4d"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e27456f513225a9edd22fc0a5f526323f6adb3099c4de87a84ceb842d93ba4"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.4.1" version = "2.4.1"
@ -1169,49 +1171,26 @@ dependencies = [
] ]
[[package]] [[package]]
name = "miow" name = "mio"
version = "0.3.7" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9"
dependencies = [ dependencies = [
"libc",
"log",
"miow",
"ntapi",
"wasi 0.11.0+wasi-snapshot-preview1",
"winapi", "winapi",
] ]
[[package]] [[package]]
name = "mlua" name = "miow"
version = "0.7.3" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d4c93ad12064932ae8f0667ecd09ca714ff44813fa1d1965ae4279108b67f21"
dependencies = [
"bstr 0.2.17",
"cc",
"erased-serde",
"futures-core",
"futures-task",
"futures-util",
"lua-src",
"luajit-src",
"mlua_derive",
"num-traits",
"once_cell",
"pkg-config",
"rustc-hash",
"serde",
]
[[package]]
name = "mlua_derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1713774a29db53a48932596dc943439dd54eb56a9efaace716719cc10fa82d5b" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [ dependencies = [
"itertools", "winapi",
"once_cell",
"proc-macro-error",
"proc-macro2",
"quote",
"regex",
"syn",
] ]
[[package]] [[package]]
@ -1230,6 +1209,34 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "normpath"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04aaf5e9cb0fbf883cc0423159eacdf96a9878022084b35c462c428cab73bcaf"
dependencies = [
"winapi",
]
[[package]]
name = "notify"
version = "5.0.0-pre.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d13c22db70a63592e098fb51735bab36646821e6389a0ba171f3549facdf0b74"
dependencies = [
"bitflags",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"mio 0.8.2",
"serde",
"walkdir",
"winapi",
]
[[package]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.3.6" version = "0.3.6"
@ -1285,12 +1292,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "oorandom"
version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.3.0" version = "0.3.0"
@ -1360,12 +1361,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "paste"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
[[package]] [[package]]
name = "pest" name = "pest"
version = "2.1.3" version = "2.1.3"
@ -1753,12 +1748,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.0" version = "0.4.0"
@ -1950,17 +1939,6 @@ dependencies = [
"libc", "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]] [[package]]
name = "siphasher" name = "siphasher"
version = "0.3.9" version = "0.3.9"
@ -2116,15 +2094,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "terminal_size" name = "terminal_size"
version = "0.1.17" version = "0.1.17"
@ -2262,7 +2231,7 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"memchr", "memchr",
"mio", "mio 0.7.13",
"num_cpus", "num_cpus",
"once_cell", "once_cell",
"parking_lot", "parking_lot",
@ -2414,6 +2383,12 @@ version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.78" version = "0.2.78"

@ -12,7 +12,7 @@ readme = "README.md"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
[workspace] [workspace]
members = ["distant-core", "distant-lua", "distant-lua-tests", "distant-ssh2"] members = ["distant-core", "distant-ssh2"]
[profile.release] [profile.release]
opt-level = 'z' opt-level = 'z'

@ -39,8 +39,6 @@ talk to the server.
Additionally, the core of the distant client and server codebase can be pulled Additionally, the core of the distant client and server codebase can be pulled
in to be used with your own Rust crates via the `distant-core` crate. in to be used with your own Rust crates via the `distant-core` crate.
Separately, Lua bindings can be found within `distant-lua`, exported as a
shared library that can be imported into lua using `require("distant_lua")`.
## Installation ## Installation

@ -16,10 +16,12 @@ bitflags = "1.3.2"
bytes = "1.1.0" bytes = "1.1.0"
chacha20poly1305 = "0.9.0" chacha20poly1305 = "0.9.0"
ciborium = "0.2.0" ciborium = "0.2.0"
derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error", "is_variant"] } derive_more = { version = "0.99.16", default-features = false, features = ["deref", "deref_mut", "display", "from", "error", "into_iterator", "is_variant"] }
futures = "0.3.16" futures = "0.3.16"
hex = "0.4.3" hex = "0.4.3"
log = "0.4.14" log = "0.4.14"
notify = { version = "5.0.0-pre.14", features = ["serde"] }
normpath = "0.3.2"
once_cell = "1.8.0" once_cell = "1.8.0"
portable-pty = "0.7.0" portable-pty = "0.7.0"
rand = { version = "0.8.4", features = ["getrandom"] } rand = { version = "0.8.4", features = ["getrandom"] }

@ -2,7 +2,9 @@ mod lsp;
mod process; mod process;
mod session; mod session;
mod utils; mod utils;
mod watcher;
pub use lsp::*; pub use lsp::*;
pub use process::*; pub use process::*;
pub use session::*; pub use session::*;
pub use watcher::*;

@ -1,8 +1,11 @@
use crate::{ use crate::{
client::{RemoteLspProcess, RemoteProcess, RemoteProcessError, SessionChannel}, client::{
RemoteLspProcess, RemoteProcess, RemoteProcessError, SessionChannel, UnwatchError,
WatchError, Watcher,
},
data::{ data::{
DirEntry, Error as Failure, Metadata, PtySize, Request, RequestData, ResponseData, ChangeKindSet, DirEntry, Error as Failure, Metadata, PtySize, Request, RequestData,
SystemInfo, ResponseData, SystemInfo,
}, },
net::TransportError, net::TransportError,
}; };
@ -118,6 +121,23 @@ pub trait SessionChannelExt {
dst: impl Into<PathBuf>, dst: impl Into<PathBuf>,
) -> AsyncReturn<'_, ()>; ) -> AsyncReturn<'_, ()>;
/// Watches a remote file or directory
fn watch(
&mut self,
tenant: impl Into<String>,
path: impl Into<PathBuf>,
recursive: bool,
only: impl Into<ChangeKindSet>,
except: impl Into<ChangeKindSet>,
) -> AsyncReturn<'_, Watcher, WatchError>;
/// Unwatches a remote file or directory
fn unwatch(
&mut self,
tenant: impl Into<String>,
path: impl Into<PathBuf>,
) -> AsyncReturn<'_, (), UnwatchError>;
/// Spawns a process on the remote machine /// Spawns a process on the remote machine
fn spawn( fn spawn(
&mut self, &mut self,
@ -374,6 +394,51 @@ impl SessionChannelExt for SessionChannel {
) )
} }
fn watch(
&mut self,
tenant: impl Into<String>,
path: impl Into<PathBuf>,
recursive: bool,
only: impl Into<ChangeKindSet>,
except: impl Into<ChangeKindSet>,
) -> AsyncReturn<'_, Watcher, WatchError> {
let tenant = tenant.into();
let path = path.into();
let only = only.into();
let except = except.into();
Box::pin(async move {
Watcher::watch(tenant, self.clone(), path, recursive, only, except).await
})
}
fn unwatch(
&mut self,
tenant: impl Into<String>,
path: impl Into<PathBuf>,
) -> AsyncReturn<'_, (), UnwatchError> {
fn inner_unwatch(
channel: &mut SessionChannel,
tenant: impl Into<String>,
path: impl Into<PathBuf>,
) -> AsyncReturn<'_, ()> {
make_body!(
channel,
tenant,
RequestData::Unwatch { path: path.into() },
@ok
)
}
let tenant = tenant.into();
let path = path.into();
Box::pin(async move {
inner_unwatch(self, tenant, path)
.await
.map_err(UnwatchError::from)
})
}
fn spawn( fn spawn(
&mut self, &mut self,
tenant: impl Into<String>, tenant: impl Into<String>,

@ -128,6 +128,11 @@ impl Session {
.await .await
.and_then(convert::identity) .and_then(convert::identity)
} }
/// Convert into underlying channel
pub fn into_channel(self) -> SessionChannel {
self.channel
}
} }
#[cfg(unix)] #[cfg(unix)]

@ -0,0 +1,544 @@
use crate::{
client::{SessionChannel, SessionChannelExt, SessionChannelExtError},
constants::CLIENT_WATCHER_CAPACITY,
data::{Change, ChangeKindSet, Error as DistantError, Request, RequestData, ResponseData},
net::TransportError,
};
use derive_more::{Display, Error, From};
use log::*;
use std::{
fmt,
path::{Path, PathBuf},
};
use tokio::{sync::mpsc, task::JoinHandle};
#[derive(Debug, Display, Error)]
pub enum WatchError {
/// When no confirmation of watch is received
MissingConfirmation,
/// A server-side error occurred when attempting to watch
ServerError(DistantError),
/// When the communication over the wire has issues
TransportError(TransportError),
/// When a queued change is dropped because the response channel closed early
QueuedChangeDropped,
/// Some unexpected response was received when attempting to watch
#[display(fmt = "Unexpected response: {:?}", _0)]
UnexpectedResponse(#[error(not(source))] ResponseData),
}
#[derive(Debug, Display, From, Error)]
pub struct UnwatchError(SessionChannelExtError);
/// Represents a watcher of some path on a remote machine
pub struct Watcher {
tenant: String,
channel: SessionChannel,
path: PathBuf,
task: JoinHandle<()>,
rx: mpsc::Receiver<Change>,
active: bool,
}
impl fmt::Debug for Watcher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Watcher")
.field("tenant", &self.tenant)
.field("path", &self.path)
.finish()
}
}
impl Watcher {
/// Creates a watcher for some remote path
pub async fn watch(
tenant: impl Into<String>,
mut channel: SessionChannel,
path: impl Into<PathBuf>,
recursive: bool,
only: impl Into<ChangeKindSet>,
except: impl Into<ChangeKindSet>,
) -> Result<Self, WatchError> {
let tenant = tenant.into();
let path = path.into();
let only = only.into();
let except = except.into();
trace!(
"Watching {:?} (recursive = {}){}{}",
path,
recursive,
if only.is_empty() {
String::new()
} else {
format!(" (only = {})", only)
},
if except.is_empty() {
String::new()
} else {
format!(" (except = {})", except)
},
);
// Submit our run request and get back a mailbox for responses
let mut mailbox = channel
.mail(Request::new(
tenant.as_str(),
vec![RequestData::Watch {
path: path.to_path_buf(),
recursive,
only: only.into_vec(),
except: except.into_vec(),
}],
))
.await
.map_err(WatchError::TransportError)?;
let (tx, rx) = mpsc::channel(CLIENT_WATCHER_CAPACITY);
// Wait to get the confirmation of watch as either ok or error
let mut queue: Vec<Change> = Vec::new();
let mut confirmed = false;
while let Some(res) = mailbox.next().await {
for data in res.payload {
match data {
ResponseData::Changed(change) => queue.push(change),
ResponseData::Ok => {
confirmed = true;
}
ResponseData::Error(x) => return Err(WatchError::ServerError(x)),
x => return Err(WatchError::UnexpectedResponse(x)),
}
}
// Exit if we got the confirmation
// NOTE: Doing this later because we want to make sure the entire payload is processed
// first before exiting the loop
if confirmed {
break;
}
}
// Send out any of our queued changes that we got prior to the acknowledgement
trace!("Forwarding {} queued changes for {:?}", queue.len(), path);
for change in queue {
if tx.send(change).await.is_err() {
return Err(WatchError::QueuedChangeDropped);
}
}
// If we never received an acknowledgement of watch before the mailbox closed,
// fail with a missing confirmation error
if !confirmed {
return Err(WatchError::MissingConfirmation);
}
// Spawn a task that continues to look for change events, discarding anything
// else that it gets
let task = tokio::spawn({
let path = path.clone();
async move {
while let Some(res) = mailbox.next().await {
for data in res.payload {
match data {
ResponseData::Changed(change) => {
// If we can't queue up a change anymore, we've
// been closed and therefore want to quit
if tx.is_closed() {
break;
}
// Otherwise, send over the change
if let Err(x) = tx.send(change).await {
error!(
"Watcher for {:?} failed to send change {:?}",
path, x.0
);
break;
}
}
_ => continue,
}
}
}
}
});
Ok(Self {
tenant,
path,
channel,
task,
rx,
active: true,
})
}
/// Returns a reference to the path this watcher is monitoring
pub fn path(&self) -> &Path {
self.path.as_path()
}
/// Returns true if the watcher is still actively watching for changes
pub fn is_active(&self) -> bool {
self.active
}
/// Returns the next change detected by the watcher, or none if the watcher has concluded
pub async fn next(&mut self) -> Option<Change> {
self.rx.recv().await
}
/// Unwatches the path being watched, closing out the watcher
pub async fn unwatch(&mut self) -> Result<(), UnwatchError> {
trace!("Unwatching {:?}", self.path);
let result = self
.channel
.unwatch(self.tenant.to_string(), self.path.to_path_buf())
.await
.map_err(UnwatchError::from);
match result {
Ok(_) => {
// Kill our task that processes inbound changes if we
// have successfully unwatched the path
self.task.abort();
self.active = false;
Ok(())
}
Err(x) => Err(x),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
client::Session,
data::{ChangeKind, Response},
net::{InmemoryStream, PlainCodec, Transport},
};
use std::sync::Arc;
use tokio::sync::Mutex;
fn make_session() -> (Transport<InmemoryStream, PlainCodec>, Session) {
let (t1, t2) = Transport::make_pair();
(t1, Session::initialize(t2).unwrap())
}
#[tokio::test]
async fn watcher_should_have_path_reflect_watched_path() {
let (mut transport, session) = make_session();
let test_path = Path::new("/some/test/path");
// Create a task for watcher as we need to handle the request and a response
// in a separate async block
let watch_task = tokio::spawn(async move {
Watcher::watch(
String::from("test-tenant"),
session.clone_channel(),
test_path,
true,
ChangeKindSet::empty(),
ChangeKindSet::empty(),
)
.await
});
// Wait until we get the request from the session
let req = transport.receive::<Request>().await.unwrap().unwrap();
// Send back an acknowledgement that a watcher was created
transport
.send(Response::new("test-tenant", req.id, vec![ResponseData::Ok]))
.await
.unwrap();
// Get the watcher and verify the path
let watcher = watch_task.await.unwrap().unwrap();
assert_eq!(watcher.path(), test_path);
}
#[tokio::test]
async fn watcher_should_support_getting_next_change() {
let (mut transport, session) = make_session();
let test_path = Path::new("/some/test/path");
// Create a task for watcher as we need to handle the request and a response
// in a separate async block
let watch_task = tokio::spawn(async move {
Watcher::watch(
String::from("test-tenant"),
session.clone_channel(),
test_path,
true,
ChangeKindSet::empty(),
ChangeKindSet::empty(),
)
.await
});
// Wait until we get the request from the session
let req = transport.receive::<Request>().await.unwrap().unwrap();
// Send back an acknowledgement that a watcher was created
transport
.send(Response::new("test-tenant", req.id, vec![ResponseData::Ok]))
.await
.unwrap();
// Get the watcher
let mut watcher = watch_task.await.unwrap().unwrap();
// Send some changes related to the file
transport
.send(Response::new(
"test-tenant",
req.id,
vec![
ResponseData::Changed(Change {
kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()],
}),
ResponseData::Changed(Change {
kind: ChangeKind::Content,
paths: vec![test_path.to_path_buf()],
}),
],
))
.await
.unwrap();
// Verify that the watcher gets the changes, one at a time
let change = watcher.next().await.expect("Watcher closed unexpectedly");
assert_eq!(
change,
Change {
kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()]
}
);
let change = watcher.next().await.expect("Watcher closed unexpectedly");
assert_eq!(
change,
Change {
kind: ChangeKind::Content,
paths: vec![test_path.to_path_buf()]
}
);
}
#[tokio::test]
async fn watcher_should_distinguish_change_events_and_only_receive_changes_for_itself() {
let (mut transport, session) = make_session();
let test_path = Path::new("/some/test/path");
// Create a task for watcher as we need to handle the request and a response
// in a separate async block
let watch_task = tokio::spawn(async move {
Watcher::watch(
String::from("test-tenant"),
session.clone_channel(),
test_path,
true,
ChangeKindSet::empty(),
ChangeKindSet::empty(),
)
.await
});
// Wait until we get the request from the session
let req = transport.receive::<Request>().await.unwrap().unwrap();
// Send back an acknowledgement that a watcher was created
transport
.send(Response::new("test-tenant", req.id, vec![ResponseData::Ok]))
.await
.unwrap();
// Get the watcher
let mut watcher = watch_task.await.unwrap().unwrap();
// Send a change from the appropriate origin
transport
.send(Response::new(
"test-tenant",
req.id,
vec![ResponseData::Changed(Change {
kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()],
})],
))
.await
.unwrap();
// Send a change from a different origin
transport
.send(Response::new(
"test-tenant",
req.id + 1,
vec![ResponseData::Changed(Change {
kind: ChangeKind::Content,
paths: vec![test_path.to_path_buf()],
})],
))
.await
.unwrap();
// Send a change from the appropriate origin
transport
.send(Response::new(
"test-tenant",
req.id,
vec![ResponseData::Changed(Change {
kind: ChangeKind::Remove,
paths: vec![test_path.to_path_buf()],
})],
))
.await
.unwrap();
// Verify that the watcher gets the changes, one at a time
let change = watcher.next().await.expect("Watcher closed unexpectedly");
assert_eq!(
change,
Change {
kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()]
}
);
let change = watcher.next().await.expect("Watcher closed unexpectedly");
assert_eq!(
change,
Change {
kind: ChangeKind::Remove,
paths: vec![test_path.to_path_buf()]
}
);
}
#[tokio::test]
async fn watcher_should_stop_receiving_events_if_unwatched() {
let (mut transport, session) = make_session();
let test_path = Path::new("/some/test/path");
// Create a task for watcher as we need to handle the request and a response
// in a separate async block
let watch_task = tokio::spawn(async move {
Watcher::watch(
String::from("test-tenant"),
session.clone_channel(),
test_path,
true,
ChangeKindSet::empty(),
ChangeKindSet::empty(),
)
.await
});
// Wait until we get the request from the session
let req = transport.receive::<Request>().await.unwrap().unwrap();
// Send back an acknowledgement that a watcher was created
transport
.send(Response::new("test-tenant", req.id, vec![ResponseData::Ok]))
.await
.unwrap();
// Send some changes from the appropriate origin
transport
.send(Response::new(
"test-tenant",
req.id,
vec![
ResponseData::Changed(Change {
kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()],
}),
ResponseData::Changed(Change {
kind: ChangeKind::Content,
paths: vec![test_path.to_path_buf()],
}),
ResponseData::Changed(Change {
kind: ChangeKind::Remove,
paths: vec![test_path.to_path_buf()],
}),
],
))
.await
.unwrap();
// Wait a little bit for all changes to be queued
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
// Create a task for for unwatching as we need to handle the request and a response
// in a separate async block
let watcher = Arc::new(Mutex::new(watch_task.await.unwrap().unwrap()));
// Verify that the watcher gets the first change
let change = watcher
.lock()
.await
.next()
.await
.expect("Watcher closed unexpectedly");
assert_eq!(
change,
Change {
kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()]
}
);
// Unwatch the watcher, verify the request is sent out, and respond with ok
let watcher_2 = Arc::clone(&watcher);
let unwatch_task = tokio::spawn(async move { watcher_2.lock().await.unwatch().await });
let req = transport.receive::<Request>().await.unwrap().unwrap();
transport
.send(Response::new("test-tenant", req.id, vec![ResponseData::Ok]))
.await
.unwrap();
// Wait for the unwatch to complete
let _ = unwatch_task.await.unwrap().unwrap();
transport
.send(Response::new(
"test-tenant",
req.id,
vec![ResponseData::Changed(Change {
kind: ChangeKind::Unknown,
paths: vec![test_path.to_path_buf()],
})],
))
.await
.unwrap();
// Verify that we get any remaining changes that were received before unwatched,
// but nothing new after that
assert_eq!(
watcher.lock().await.next().await,
Some(Change {
kind: ChangeKind::Content,
paths: vec![test_path.to_path_buf()]
})
);
assert_eq!(
watcher.lock().await.next().await,
Some(Change {
kind: ChangeKind::Remove,
paths: vec![test_path.to_path_buf()]
})
);
assert_eq!(watcher.lock().await.next().await, None);
}
}

@ -4,6 +4,9 @@ pub const CLIENT_MAILBOX_CAPACITY: usize = 10000;
/// Capacity associated stdin, stdout, and stderr pipes receiving data from remote server /// Capacity associated stdin, stdout, and stderr pipes receiving data from remote server
pub const CLIENT_PIPE_CAPACITY: usize = 10000; pub const CLIENT_PIPE_CAPACITY: usize = 10000;
/// Capacity associated with a client watcher receiving changes
pub const CLIENT_WATCHER_CAPACITY: usize = 100;
/// Represents the maximum size (in bytes) that data will be read from pipes /// Represents the maximum size (in bytes) that data will be read from pipes
/// per individual `read` call /// per individual `read` call
/// ///

@ -1,9 +1,15 @@
use bitflags::bitflags; use bitflags::bitflags;
use derive_more::{Display, Error, IsVariant}; use derive_more::{Deref, DerefMut, Display, Error, IntoIterator, IsVariant};
use notify::{
event::Event as NotifyEvent, ErrorKind as NotifyErrorKind, EventKind as NotifyEventKind,
};
use portable_pty::PtySize as PortablePtySize; use portable_pty::PtySize as PortablePtySize;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{io, num::ParseIntError, path::PathBuf, str::FromStr}; use std::{
use strum::AsRefStr; collections::HashSet, io, iter::FromIterator, num::ParseIntError, ops::BitOr, path::PathBuf,
str::FromStr,
};
use strum::{AsRefStr, EnumString, EnumVariantNames, VariantNames};
/// Type alias for a vec of bytes /// Type alias for a vec of bytes
/// ///
@ -189,6 +195,39 @@ pub enum RequestData {
dst: PathBuf, dst: PathBuf,
}, },
/// Watches a path for changes
Watch {
/// The path to the file, directory, or symlink on the remote machine
path: PathBuf,
/// If true, will recursively watch for changes within directories, othewise
/// will only watch for changes immediately within directories
#[cfg_attr(feature = "structopt", structopt(short, long))]
recursive: bool,
/// Filter to only report back specified changes
#[cfg_attr(
feature = "structopt",
structopt(short, long, possible_values = &ChangeKind::VARIANTS)
)]
#[serde(default)]
only: Vec<ChangeKind>,
/// Filter to report back changes except these specified changes
#[cfg_attr(
feature = "structopt",
structopt(short, long, possible_values = &ChangeKind::VARIANTS)
)]
#[serde(default)]
except: Vec<ChangeKind>,
},
/// Unwatches a path for changes, meaning no additional changes will be reported
Unwatch {
/// The path to the file, directory, or symlink on the remote machine
path: PathBuf,
},
/// Checks whether the given path exists /// Checks whether the given path exists
Exists { Exists {
/// The path to the file or directory on the remote machine /// The path to the file or directory on the remote machine
@ -336,6 +375,9 @@ pub enum ResponseData {
errors: Vec<Error>, errors: Vec<Error>,
}, },
/// Response to a filesystem change for some watched file, directory, or symlink
Changed(Change),
/// Response to checking if a path exists /// Response to checking if a path exists
Exists { value: bool }, Exists { value: bool },
@ -910,12 +952,446 @@ impl From<walkdir::Error> for ResponseData {
} }
} }
impl From<notify::Error> for ResponseData {
fn from(x: notify::Error) -> Self {
Self::Error(Error::from(x))
}
}
impl From<tokio::task::JoinError> for ResponseData { impl From<tokio::task::JoinError> for ResponseData {
fn from(x: tokio::task::JoinError) -> Self { fn from(x: tokio::task::JoinError) -> Self {
Self::Error(Error::from(x)) Self::Error(Error::from(x))
} }
} }
/// Change to one or more paths on the filesystem
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct Change {
/// Label describing the kind of change
pub kind: ChangeKind,
/// Paths that were changed
pub paths: Vec<PathBuf>,
}
impl From<NotifyEvent> for Change {
fn from(x: NotifyEvent) -> Self {
Self {
kind: x.kind.into(),
paths: x.paths,
}
}
}
#[derive(
Copy,
Clone,
Debug,
strum::Display,
EnumString,
EnumVariantNames,
Hash,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
#[strum(serialize_all = "snake_case")]
pub enum ChangeKind {
/// Something about a file or directory was accessed, but
/// no specific details were known
Access,
/// A file was closed for executing
AccessCloseExecute,
/// A file was closed for reading
AccessCloseRead,
/// A file was closed for writing
AccessCloseWrite,
/// A file was opened for executing
AccessOpenExecute,
/// A file was opened for reading
AccessOpenRead,
/// A file was opened for writing
AccessOpenWrite,
/// A file or directory was read
AccessRead,
/// The access time of a file or directory was changed
AccessTime,
/// A file, directory, or something else was created
Create,
/// The content of a file or directory changed
Content,
/// The data of a file or directory was modified, but
/// no specific details were known
Data,
/// The metadata of a file or directory was modified, but
/// no specific details were known
Metadata,
/// Something about a file or directory was modified, but
/// no specific details were known
Modify,
/// A file, directory, or something else was removed
Remove,
/// A file or directory was renamed, but no specific details were known
Rename,
/// A file or directory was renamed, and the provided paths
/// are the source and target in that order (from, to)
RenameBoth,
/// A file or directory was renamed, and the provided path
/// is the origin of the rename (before being renamed)
RenameFrom,
/// A file or directory was renamed, and the provided path
/// is the result of the rename
RenameTo,
/// A file's size changed
Size,
/// The ownership of a file or directory was changed
Ownership,
/// The permissions of a file or directory was changed
Permissions,
/// The write or modify time of a file or directory was changed
WriteTime,
// Catchall in case we have no insight as to the type of change
Unknown,
}
impl ChangeKind {
/// Returns true if the change is a kind of access
pub fn is_access_kind(&self) -> bool {
self.is_open_access_kind()
|| self.is_close_access_kind()
|| matches!(self, Self::Access | Self::AccessRead)
}
/// Returns true if the change is a kind of open access
pub fn is_open_access_kind(&self) -> bool {
matches!(
self,
Self::AccessOpenExecute | Self::AccessOpenRead | Self::AccessOpenWrite
)
}
/// Returns true if the change is a kind of close access
pub fn is_close_access_kind(&self) -> bool {
matches!(
self,
Self::AccessCloseExecute | Self::AccessCloseRead | Self::AccessCloseWrite
)
}
/// Returns true if the change is a kind of creation
pub fn is_create_kind(&self) -> bool {
matches!(self, Self::Create)
}
/// Returns true if the change is a kind of modification
pub fn is_modify_kind(&self) -> bool {
self.is_data_modify_kind() || self.is_metadata_modify_kind() || matches!(self, Self::Modify)
}
/// Returns true if the change is a kind of data modification
pub fn is_data_modify_kind(&self) -> bool {
matches!(self, Self::Content | Self::Data | Self::Size)
}
/// Returns true if the change is a kind of metadata modification
pub fn is_metadata_modify_kind(&self) -> bool {
matches!(
self,
Self::AccessTime
| Self::Metadata
| Self::Ownership
| Self::Permissions
| Self::WriteTime
)
}
/// Returns true if the change is a kind of rename
pub fn is_rename_kind(&self) -> bool {
matches!(
self,
Self::Rename | Self::RenameBoth | Self::RenameFrom | Self::RenameTo
)
}
/// Returns true if the change is a kind of removal
pub fn is_remove_kind(&self) -> bool {
matches!(self, Self::Remove)
}
/// Returns true if the change kind is unknown
pub fn is_unknown_kind(&self) -> bool {
matches!(self, Self::Unknown)
}
}
impl BitOr for ChangeKind {
type Output = ChangeKindSet;
fn bitor(self, rhs: Self) -> Self::Output {
let mut set = ChangeKindSet::empty();
set.insert(self);
set.insert(rhs);
set
}
}
impl From<NotifyEventKind> for ChangeKind {
fn from(x: NotifyEventKind) -> Self {
use notify::event::{
AccessKind, AccessMode, DataChange, MetadataKind, ModifyKind, RenameMode,
};
match x {
// File/directory access events
NotifyEventKind::Access(AccessKind::Read) => Self::AccessRead,
NotifyEventKind::Access(AccessKind::Open(AccessMode::Execute)) => {
Self::AccessOpenExecute
}
NotifyEventKind::Access(AccessKind::Open(AccessMode::Read)) => Self::AccessOpenRead,
NotifyEventKind::Access(AccessKind::Open(AccessMode::Write)) => Self::AccessOpenWrite,
NotifyEventKind::Access(AccessKind::Close(AccessMode::Execute)) => {
Self::AccessCloseExecute
}
NotifyEventKind::Access(AccessKind::Close(AccessMode::Read)) => Self::AccessCloseRead,
NotifyEventKind::Access(AccessKind::Close(AccessMode::Write)) => Self::AccessCloseWrite,
NotifyEventKind::Access(_) => Self::Access,
// File/directory creation events
NotifyEventKind::Create(_) => Self::Create,
// Rename-oriented events
NotifyEventKind::Modify(ModifyKind::Name(RenameMode::Both)) => Self::RenameBoth,
NotifyEventKind::Modify(ModifyKind::Name(RenameMode::From)) => Self::RenameFrom,
NotifyEventKind::Modify(ModifyKind::Name(RenameMode::To)) => Self::RenameTo,
NotifyEventKind::Modify(ModifyKind::Name(_)) => Self::Rename,
// Data-modification events
NotifyEventKind::Modify(ModifyKind::Data(DataChange::Content)) => Self::Content,
NotifyEventKind::Modify(ModifyKind::Data(DataChange::Size)) => Self::Size,
NotifyEventKind::Modify(ModifyKind::Data(_)) => Self::Data,
// Metadata-modification events
NotifyEventKind::Modify(ModifyKind::Metadata(MetadataKind::AccessTime)) => {
Self::AccessTime
}
NotifyEventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => {
Self::WriteTime
}
NotifyEventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions)) => {
Self::Permissions
}
NotifyEventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)) => {
Self::Ownership
}
NotifyEventKind::Modify(ModifyKind::Metadata(_)) => Self::Metadata,
// General modification events
NotifyEventKind::Modify(_) => Self::Modify,
// File/directory removal events
NotifyEventKind::Remove(_) => Self::Remove,
// Catch-all for other events
NotifyEventKind::Any | NotifyEventKind::Other => Self::Unknown,
}
}
}
/// Represents a distinct set of different change kinds
#[derive(
Clone, Debug, Deref, DerefMut, Display, IntoIterator, PartialEq, Eq, Serialize, Deserialize,
)]
#[display(
fmt = "{}",
"_0.iter().map(ToString::to_string).collect::<Vec<String>>().join(\",\")"
)]
pub struct ChangeKindSet(HashSet<ChangeKind>);
impl ChangeKindSet {
/// Produces an empty set of [`ChangeKind`]
pub fn empty() -> Self {
Self(HashSet::new())
}
/// Produces a set of all [`ChangeKind`]
pub fn all() -> Self {
vec![
ChangeKind::Access,
ChangeKind::AccessCloseExecute,
ChangeKind::AccessCloseRead,
ChangeKind::AccessCloseWrite,
ChangeKind::AccessOpenExecute,
ChangeKind::AccessOpenRead,
ChangeKind::AccessOpenWrite,
ChangeKind::AccessRead,
ChangeKind::AccessTime,
ChangeKind::Create,
ChangeKind::Content,
ChangeKind::Data,
ChangeKind::Metadata,
ChangeKind::Modify,
ChangeKind::Remove,
ChangeKind::Rename,
ChangeKind::RenameBoth,
ChangeKind::RenameFrom,
ChangeKind::RenameTo,
ChangeKind::Size,
ChangeKind::Ownership,
ChangeKind::Permissions,
ChangeKind::WriteTime,
ChangeKind::Unknown,
]
.into_iter()
.collect()
}
/// Produces a changeset containing all of the access kinds
pub fn access_set() -> Self {
Self::access_open_set()
| Self::access_close_set()
| ChangeKind::AccessRead
| ChangeKind::Access
}
/// Produces a changeset containing all of the open access kinds
pub fn access_open_set() -> Self {
ChangeKind::AccessOpenExecute | ChangeKind::AccessOpenRead | ChangeKind::AccessOpenWrite
}
/// Produces a changeset containing all of the close access kinds
pub fn access_close_set() -> Self {
ChangeKind::AccessCloseExecute | ChangeKind::AccessCloseRead | ChangeKind::AccessCloseWrite
}
// Produces a changeset containing all of the modification kinds
pub fn modify_set() -> Self {
Self::modify_data_set() | Self::modify_metadata_set() | ChangeKind::Modify
}
/// Produces a changeset containing all of the data modification kinds
pub fn modify_data_set() -> Self {
ChangeKind::Content | ChangeKind::Data | ChangeKind::Size
}
/// Produces a changeset containing all of the metadata modification kinds
pub fn modify_metadata_set() -> Self {
ChangeKind::AccessTime
| ChangeKind::Metadata
| ChangeKind::Ownership
| ChangeKind::Permissions
| ChangeKind::WriteTime
}
/// Produces a changeset containing all of the rename kinds
pub fn rename_set() -> Self {
ChangeKind::Rename | ChangeKind::RenameBoth | ChangeKind::RenameFrom | ChangeKind::RenameTo
}
/// Consumes set and returns a vec of the kinds of changes
pub fn into_vec(self) -> Vec<ChangeKind> {
self.0.into_iter().collect()
}
}
impl BitOr<ChangeKindSet> for ChangeKindSet {
type Output = Self;
fn bitor(mut self, rhs: ChangeKindSet) -> Self::Output {
self.extend(rhs.0);
self
}
}
impl BitOr<ChangeKind> for ChangeKindSet {
type Output = Self;
fn bitor(mut self, rhs: ChangeKind) -> Self::Output {
self.0.insert(rhs);
self
}
}
impl BitOr<ChangeKindSet> for ChangeKind {
type Output = ChangeKindSet;
fn bitor(self, rhs: ChangeKindSet) -> Self::Output {
rhs | self
}
}
impl FromStr for ChangeKindSet {
type Err = strum::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut change_set = HashSet::new();
for word in s.split(',') {
change_set.insert(ChangeKind::from_str(word.trim())?);
}
Ok(ChangeKindSet(change_set))
}
}
impl FromIterator<ChangeKind> for ChangeKindSet {
fn from_iter<I: IntoIterator<Item = ChangeKind>>(iter: I) -> Self {
let mut change_set = HashSet::new();
for i in iter {
change_set.insert(i);
}
ChangeKindSet(change_set)
}
}
impl From<ChangeKind> for ChangeKindSet {
fn from(change_kind: ChangeKind) -> Self {
let mut set = Self::empty();
set.insert(change_kind);
set
}
}
impl From<Vec<ChangeKind>> for ChangeKindSet {
fn from(changes: Vec<ChangeKind>) -> Self {
changes.into_iter().collect()
}
}
impl Default for ChangeKindSet {
fn default() -> Self {
Self::empty()
}
}
/// General purpose error type that can be sent across the wire /// General purpose error type that can be sent across the wire
#[derive(Clone, Debug, Display, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, Display, PartialEq, Eq, Serialize, Deserialize)]
#[display(fmt = "{}: {}", kind, description)] #[display(fmt = "{}: {}", kind, description)]
@ -930,6 +1406,21 @@ pub struct Error {
impl std::error::Error for Error {} impl std::error::Error for Error {}
impl<'a> From<&'a str> for Error {
fn from(x: &'a str) -> Self {
Self::from(x.to_string())
}
}
impl From<String> for Error {
fn from(x: String) -> Self {
Self {
kind: ErrorKind::Other,
description: x,
}
}
}
impl From<io::Error> for Error { impl From<io::Error> for Error {
fn from(x: io::Error) -> Self { fn from(x: io::Error) -> Self {
Self { Self {
@ -945,6 +1436,47 @@ impl From<Error> for io::Error {
} }
} }
impl From<notify::Error> for Error {
fn from(x: notify::Error) -> Self {
let err = match x.kind {
NotifyErrorKind::Generic(x) => Self {
kind: ErrorKind::Other,
description: x,
},
NotifyErrorKind::Io(x) => Self::from(x),
NotifyErrorKind::PathNotFound => Self {
kind: ErrorKind::Other,
description: String::from("Path not found"),
},
NotifyErrorKind::WatchNotFound => Self {
kind: ErrorKind::Other,
description: String::from("Watch not found"),
},
NotifyErrorKind::InvalidConfig(_) => Self {
kind: ErrorKind::Other,
description: String::from("Invalid config"),
},
NotifyErrorKind::MaxFilesWatch => Self {
kind: ErrorKind::Other,
description: String::from("Max files watched"),
},
};
Self {
kind: err.kind,
description: format!(
"{}\n\nPaths: {}",
err.description,
x.paths
.into_iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<String>>()
.join(", ")
),
}
}
}
impl From<walkdir::Error> for Error { impl From<walkdir::Error> for Error {
fn from(x: walkdir::Error) -> Self { fn from(x: walkdir::Error) -> Self {
if x.io_error().is_some() { if x.io_error().is_some() {

@ -1,16 +1,17 @@
use crate::{ use crate::{
data::{ data::{
self, DirEntry, FileType, Metadata, PtySize, Request, RequestData, Response, ResponseData, self, Change, ChangeKind, ChangeKindSet, DirEntry, FileType, Metadata, PtySize, Request,
RunningProcess, SystemInfo, RequestData, Response, ResponseData, RunningProcess, SystemInfo,
}, },
server::distant::{ server::distant::{
process::{Process, PtyProcess, SimpleProcess}, process::{Process, PtyProcess, SimpleProcess},
state::{ProcessState, State}, state::{ProcessState, State, WatcherPath},
}, },
}; };
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use futures::future; use futures::future;
use log::*; use log::*;
use notify::{Config as WatcherConfig, RecursiveMode, Watcher};
use std::{ use std::{
env, env,
future::Future, future::Future,
@ -30,15 +31,17 @@ type ReplyRet = Pin<Box<dyn Future<Output = bool> + Send + 'static>>;
#[derive(Debug, Display, Error, From)] #[derive(Debug, Display, Error, From)]
pub enum ServerError { pub enum ServerError {
IoError(io::Error), Io(io::Error),
WalkDirError(walkdir::Error), Notify(notify::Error),
WalkDir(walkdir::Error),
} }
impl From<ServerError> for ResponseData { impl From<ServerError> for ResponseData {
fn from(x: ServerError) -> Self { fn from(x: ServerError) -> Self {
match x { match x {
ServerError::IoError(x) => Self::from(x), ServerError::Io(x) => Self::from(x),
ServerError::WalkDirError(x) => Self::from(x), ServerError::Notify(x) => Self::from(x),
ServerError::WalkDir(x) => Self::from(x),
} }
} }
} }
@ -92,6 +95,13 @@ pub(super) async fn process(
RequestData::Remove { path, force } => remove(path, force).await, RequestData::Remove { path, force } => remove(path, force).await,
RequestData::Copy { src, dst } => copy(src, dst).await, RequestData::Copy { src, dst } => copy(src, dst).await,
RequestData::Rename { src, dst } => rename(src, dst).await, RequestData::Rename { src, dst } => rename(src, dst).await,
RequestData::Watch {
path,
recursive,
only,
except,
} => watch(conn_id, state, reply, path, recursive, only, except).await,
RequestData::Unwatch { path } => unwatch(conn_id, state, path).await,
RequestData::Exists { path } => exists(path).await, RequestData::Exists { path } => exists(path).await,
RequestData::Metadata { RequestData::Metadata {
path, path,
@ -366,6 +376,205 @@ async fn rename(src: PathBuf, dst: PathBuf) -> Result<Outgoing, ServerError> {
Ok(Outgoing::from(ResponseData::Ok)) Ok(Outgoing::from(ResponseData::Ok))
} }
async fn watch<F>(
conn_id: usize,
state: HState,
reply: F,
path: PathBuf,
recursive: bool,
only: Vec<ChangeKind>,
except: Vec<ChangeKind>,
) -> Result<Outgoing, ServerError>
where
F: FnMut(Vec<ResponseData>) -> ReplyRet + Clone + Send + 'static,
{
let only = only.into_iter().collect::<ChangeKindSet>();
let except = except.into_iter().collect::<ChangeKindSet>();
let state_2 = Arc::clone(&state);
let mut state = state.lock().await;
// NOTE: Do not use get_or_insert_with since notify::recommended_watcher returns a result
// and we cannot unpack the result within the above function. Since we are locking
// our state, we can be confident that no one else is modifying the watcher option
// concurrently; so, we do a naive check for option being populated
if state.watcher.is_none() {
let (tx, mut rx) = mpsc::channel(1);
let mut watcher = notify::recommended_watcher(move |res| {
let _ = tx.blocking_send(res);
})?;
// Attempt to configure watcher, but don't fail if these configurations fail
match watcher.configure(WatcherConfig::PreciseEvents(true)) {
Ok(true) => debug!("<Conn @ {}> Watcher configured for precise events", conn_id,),
Ok(false) => debug!(
"<Conn @ {}> Watcher not configured for precise events",
conn_id,
),
Err(x) => error!(
"<Conn @ {}> Watcher configuration for precise events failed: {}",
conn_id, x
),
}
// Attempt to configure watcher, but don't fail if these configurations fail
match watcher.configure(WatcherConfig::NoticeEvents(true)) {
Ok(true) => debug!("<Conn @ {}> Watcher configured for notice events", conn_id),
Ok(false) => debug!(
"<Conn @ {}> Watcher not configured for notice events",
conn_id,
),
Err(x) => error!(
"<Conn @ {}> Watcher configuration for notice events failed: {}",
conn_id, x
),
}
let _ = state.watcher.insert(watcher);
tokio::spawn(async move {
while let Some(res) = rx.recv().await {
let is_ok = match res {
Ok(mut x) => {
let mut state = state_2.lock().await;
let paths: Vec<_> = x.paths.drain(..).collect();
let kind = ChangeKind::from(x.kind);
trace!(
"<Conn @ {}> Watcher detected '{}' change for {:?}",
conn_id,
kind,
paths
);
fn make_res_data(kind: ChangeKind, paths: &[&PathBuf]) -> ResponseData {
ResponseData::Changed(Change {
kind,
paths: paths.iter().map(|p| p.to_path_buf()).collect(),
})
}
let results = state.map_paths_to_watcher_paths_and_replies(&paths);
let mut is_ok = true;
for (paths, only, reply) in results {
// Skip sending this change if we are not watching it
if (!only.is_empty() && !only.contains(&kind))
|| (!except.is_empty() && except.contains(&kind))
{
trace!(
"<Conn @ {}> Skipping change '{}' for {:?}",
conn_id,
kind,
paths
);
continue;
}
if !reply(vec![make_res_data(kind, &paths)]).await {
is_ok = false;
break;
}
}
is_ok
}
Err(mut x) => {
let mut state = state_2.lock().await;
let paths: Vec<_> = x.paths.drain(..).collect();
let msg = x.to_string();
error!(
"<Conn @ {}> Watcher encountered an error {} for {:?}",
conn_id, msg, paths
);
fn make_res_data(msg: &str, paths: &[&PathBuf]) -> ResponseData {
if paths.is_empty() {
ResponseData::Error(msg.into())
} else {
ResponseData::Error(format!("{} about {:?}", msg, paths).into())
}
}
let mut is_ok = true;
// If we have no paths for the errors, then we send the error to everyone
if paths.is_empty() {
trace!("<Conn @ {}> Relaying error to all watchers", conn_id);
for reply in state.watcher_paths.values_mut() {
if !reply(vec![make_res_data(&msg, &[])]).await {
is_ok = false;
break;
}
}
// Otherwise, figure out the relevant watchers from our paths and
// send the error to them
} else {
let results = state.map_paths_to_watcher_paths_and_replies(&paths);
trace!(
"<Conn @ {}> Relaying error to {} watchers",
conn_id,
results.len()
);
for (paths, _, reply) in results {
if !reply(vec![make_res_data(&msg, &paths)]).await {
is_ok = false;
break;
}
}
}
is_ok
}
};
if !is_ok {
error!("<Conn @ {}> Watcher channel closed", conn_id);
break;
}
}
});
}
match state.watcher.as_mut() {
Some(watcher) => {
let wp = WatcherPath::new(&path, recursive, only)?;
watcher.watch(
path.as_path(),
if recursive {
RecursiveMode::Recursive
} else {
RecursiveMode::NonRecursive
},
)?;
state.watcher_paths.insert(wp, Box::new(reply));
Ok(Outgoing::from(ResponseData::Ok))
}
None => Err(ServerError::Io(io::Error::new(
io::ErrorKind::BrokenPipe,
format!("<Conn @ {}> Unable to initialize watcher", conn_id,),
))),
}
}
async fn unwatch(conn_id: usize, state: HState, path: PathBuf) -> Result<Outgoing, ServerError> {
if let Some(watcher) = state.lock().await.watcher.as_mut() {
watcher.unwatch(path.as_path())?;
// TODO: This also needs to remove any path that matches in either raw form
// or canonicalized form from the map of PathBuf -> ReplyFn
return Ok(Outgoing::from(ResponseData::Ok));
}
Err(ServerError::Io(io::Error::new(
io::ErrorKind::BrokenPipe,
format!(
"<Conn @ {}> Unable to unwatch as watcher not initialized",
conn_id,
),
)))
}
async fn exists(path: PathBuf) -> Result<Outgoing, ServerError> { async fn exists(path: PathBuf) -> Result<Outgoing, ServerError> {
// Following experimental `std::fs::try_exists`, which checks the error kind of the // Following experimental `std::fs::try_exists`, which checks the error kind of the
// metadata lookup to see if it is not found and filters accordingly // metadata lookup to see if it is not found and filters accordingly
@ -587,7 +796,7 @@ async fn proc_kill(conn_id: usize, state: HState, id: usize) -> Result<Outgoing,
} }
} }
Err(ServerError::IoError(io::Error::new( Err(ServerError::Io(io::Error::new(
io::ErrorKind::BrokenPipe, io::ErrorKind::BrokenPipe,
format!( format!(
"<Conn @ {} | Proc {}> Unable to send kill signal to process", "<Conn @ {} | Proc {}> Unable to send kill signal to process",
@ -610,7 +819,7 @@ async fn proc_stdin(
} }
} }
Err(ServerError::IoError(io::Error::new( Err(ServerError::Io(io::Error::new(
io::ErrorKind::BrokenPipe, io::ErrorKind::BrokenPipe,
format!( format!(
"<Conn @ {} | Proc {}> Unable to send stdin to process", "<Conn @ {} | Proc {}> Unable to send stdin to process",
@ -631,7 +840,7 @@ async fn proc_resize_pty(
return Ok(Outgoing::from(ResponseData::Ok)); return Ok(Outgoing::from(ResponseData::Ok));
} }
Err(ServerError::IoError(io::Error::new( Err(ServerError::Io(io::Error::new(
io::ErrorKind::BrokenPipe, io::ErrorKind::BrokenPipe,
format!( format!(
"<Conn @ {} | Proc {}> Unable to resize pty to {:?}", "<Conn @ {} | Proc {}> Unable to resize pty to {:?}",
@ -1906,6 +2115,289 @@ mod tests {
dst.assert("some text"); dst.assert("some text");
} }
/// Validates a response as being a series of changes that include the provided paths
fn validate_changed_paths(
res: &Response,
expected_paths: &[PathBuf],
should_panic: bool,
) -> bool {
match &res.payload[0] {
ResponseData::Changed(change) if should_panic => {
let paths: Vec<PathBuf> = change
.paths
.iter()
.map(|x| x.canonicalize().unwrap())
.collect();
assert_eq!(paths, expected_paths, "Wrong paths reported: {:?}", change);
true
}
ResponseData::Changed(change) => {
let paths: Vec<PathBuf> = change
.paths
.iter()
.map(|x| x.canonicalize().unwrap())
.collect();
paths == expected_paths
}
x if should_panic => panic!("Unexpected response: {:?}", x),
_ => false,
}
}
#[tokio::test]
async fn watch_should_support_watching_a_single_file() {
// NOTE: Supporting multiple replies being sent back as part of creating, modifying, etc.
let (conn_id, state, tx, mut rx) = setup(100);
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.touch().unwrap();
let req = Request::new(
"test-tenant",
vec![RequestData::Watch {
path: file.path().to_path_buf(),
recursive: false,
only: Default::default(),
except: Default::default(),
}],
);
// NOTE: We need to clone state so we don't drop the watcher
// as part of dropping the state
process(conn_id, Arc::clone(&state), req, tx).await.unwrap();
let res = rx.recv().await.unwrap();
assert_eq!(res.payload.len(), 1, "Wrong payload size");
assert!(
matches!(res.payload[0], ResponseData::Ok),
"Unexpected response: {:?}",
res.payload[0]
);
// Update the file and verify we get a notification
file.write_str("some text").unwrap();
let res = rx
.recv()
.await
.expect("Channel closed before we got change");
assert_eq!(res.payload.len(), 1, "Wrong payload size");
validate_changed_paths(
&res,
&[file.path().to_path_buf().canonicalize().unwrap()],
/* should_panic */ true,
);
}
#[tokio::test]
async fn watch_should_support_watching_a_directory_recursively() {
// NOTE: Supporting multiple replies being sent back as part of creating, modifying, etc.
let (conn_id, state, tx, mut rx) = setup(100);
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.touch().unwrap();
let dir = temp.child("dir");
dir.create_dir_all().unwrap();
let req = Request::new(
"test-tenant",
vec![RequestData::Watch {
path: temp.path().to_path_buf(),
recursive: true,
only: Default::default(),
except: Default::default(),
}],
);
// NOTE: We need to clone state so we don't drop the watcher
// as part of dropping the state
process(conn_id, Arc::clone(&state), req, tx).await.unwrap();
let res = rx.recv().await.unwrap();
assert_eq!(res.payload.len(), 1, "Wrong payload size");
assert!(
matches!(res.payload[0], ResponseData::Ok),
"Unexpected response: {:?}",
res.payload[0]
);
// Update the file and verify we get a notification
file.write_str("some text").unwrap();
// Create a nested file and verify we get a notification
let nested_file = dir.child("nested-file");
nested_file.write_str("some text").unwrap();
// Sleep a bit to give time to get all changes happening
// TODO: Can we slim down this sleep? Or redesign test in some other way?
tokio::time::sleep(Duration::from_millis(100)).await;
// Collect all responses, as we may get multiple for interactions within a directory
let mut responses = Vec::new();
while let Ok(res) = rx.try_recv() {
responses.push(res);
}
// Validate that we have at least one change reported for each of our paths
assert!(
responses.len() >= 2,
"Less than expected total responses: {:?}",
responses
);
let path = file.path().to_path_buf();
assert!(
responses.iter().any(|res| validate_changed_paths(
res,
&[file.path().to_path_buf().canonicalize().unwrap()],
/* should_panic */ false,
)),
"Missing {:?} in {:?}",
path,
responses
.iter()
.map(|x| format!("{:?}", x))
.collect::<Vec<String>>(),
);
let path = nested_file.path().to_path_buf();
assert!(
responses.iter().any(|res| validate_changed_paths(
res,
&[file.path().to_path_buf().canonicalize().unwrap()],
/* should_panic */ false,
)),
"Missing {:?} in {:?}",
path,
responses
.iter()
.map(|x| format!("{:?}", x))
.collect::<Vec<String>>(),
);
}
#[tokio::test]
async fn watch_should_report_changes_using_the_request_id() {
// NOTE: Supporting multiple replies being sent back as part of creating, modifying, etc.
let (conn_id, state, tx, mut rx) = setup(100);
let temp = assert_fs::TempDir::new().unwrap();
let file_1 = temp.child("file_1");
file_1.touch().unwrap();
let file_2 = temp.child("file_2");
file_2.touch().unwrap();
// Sleep a bit to give time to get all changes happening
// TODO: Can we slim down this sleep? Or redesign test in some other way?
tokio::time::sleep(Duration::from_millis(100)).await;
// Initialize watch on file 1
let file_1_origin_id = {
let req = Request::new(
"test-tenant",
vec![RequestData::Watch {
path: file_1.path().to_path_buf(),
recursive: false,
only: Default::default(),
except: Default::default(),
}],
);
let origin_id = req.id;
// NOTE: We need to clone state so we don't drop the watcher
// as part of dropping the state
process(conn_id, Arc::clone(&state), req, tx.clone())
.await
.unwrap();
let res = rx.recv().await.unwrap();
assert_eq!(res.payload.len(), 1, "Wrong payload size");
assert!(
matches!(res.payload[0], ResponseData::Ok),
"Unexpected response: {:?}",
res.payload[0]
);
origin_id
};
// Initialize watch on file 2
let file_2_origin_id = {
let req = Request::new(
"test-tenant",
vec![RequestData::Watch {
path: file_2.path().to_path_buf(),
recursive: false,
only: Default::default(),
except: Default::default(),
}],
);
let origin_id = req.id;
// NOTE: We need to clone state so we don't drop the watcher
// as part of dropping the state
process(conn_id, Arc::clone(&state), req, tx).await.unwrap();
let res = rx.recv().await.unwrap();
assert_eq!(res.payload.len(), 1, "Wrong payload size");
assert!(
matches!(res.payload[0], ResponseData::Ok),
"Unexpected response: {:?}",
res.payload[0]
);
origin_id
};
// Update the files and verify we get notifications from different origins
{
file_1.write_str("some text").unwrap();
let res = rx
.recv()
.await
.expect("Channel closed before we got change");
assert_eq!(res.payload.len(), 1, "Wrong payload size");
validate_changed_paths(
&res,
&[file_1.path().to_path_buf().canonicalize().unwrap()],
/* should_panic */ true,
);
assert_eq!(res.origin_id, file_1_origin_id, "Wrong origin id (file 1)");
// Process any extra messages (we might get create, content, and more)
loop {
// Sleep a bit to give time to get all changes happening
// TODO: Can we slim down this sleep? Or redesign test in some other way?
tokio::time::sleep(Duration::from_millis(100)).await;
if rx.try_recv().is_err() {
break;
}
}
}
// Update the files and verify we get notifications from different origins
{
file_2.write_str("some text").unwrap();
let res = rx
.recv()
.await
.expect("Channel closed before we got change");
assert_eq!(res.payload.len(), 1, "Wrong payload size");
validate_changed_paths(
&res,
&[file_2.path().to_path_buf().canonicalize().unwrap()],
/* should_panic */ true,
);
assert_eq!(res.origin_id, file_2_origin_id, "Wrong origin id (file 2)");
}
}
#[tokio::test] #[tokio::test]
async fn exists_should_send_true_if_path_exists() { async fn exists_should_send_true_if_path_exists() {
let (conn_id, state, tx, mut rx) = setup(1); let (conn_id, state, tx, mut rx) = setup(1);

@ -1,6 +1,19 @@
use super::{InputChannel, ProcessKiller, ProcessPty}; use super::{InputChannel, ProcessKiller, ProcessPty};
use crate::data::{ChangeKindSet, ResponseData};
use log::*; use log::*;
use std::collections::HashMap; use notify::RecommendedWatcher;
use std::{
collections::HashMap,
future::Future,
hash::{Hash, Hasher},
io,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
pin::Pin,
};
pub type ReplyFn = Box<dyn FnMut(Vec<ResponseData>) -> ReplyRet + Send + 'static>;
pub type ReplyRet = Pin<Box<dyn Future<Output = bool> + Send + 'static>>;
/// Holds state related to multiple connections managed by a server /// Holds state related to multiple connections managed by a server
#[derive(Default)] #[derive(Default)]
@ -10,6 +23,109 @@ pub struct State {
/// List of processes that will be killed when a connection drops /// List of processes that will be killed when a connection drops
client_processes: HashMap<usize, Vec<usize>>, client_processes: HashMap<usize, Vec<usize>>,
/// Watcher used for filesystem events
pub watcher: Option<RecommendedWatcher>,
/// Mapping of Path -> (Reply Fn, recursive) for watcher notifications
pub watcher_paths: HashMap<WatcherPath, ReplyFn>,
}
#[derive(Clone, Debug)]
pub struct WatcherPath {
/// The raw path provided to the watcher, which is not canonicalized
raw_path: PathBuf,
/// The canonicalized path at the time of providing to the watcher,
/// as all paths must exist for a watcher, we use this to get the
/// source of truth when watching
path: PathBuf,
/// Whether or not the path was set to be recursive
recursive: bool,
/// Specific filter for path
only: ChangeKindSet,
}
impl PartialEq for WatcherPath {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl Eq for WatcherPath {}
impl Hash for WatcherPath {
fn hash<H: Hasher>(&self, state: &mut H) {
self.path.hash(state);
}
}
impl Deref for WatcherPath {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.path
}
}
impl DerefMut for WatcherPath {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.path
}
}
impl WatcherPath {
/// Create a new watcher path using the given path and canonicalizing it
pub fn new(
path: impl Into<PathBuf>,
recursive: bool,
only: impl Into<ChangeKindSet>,
) -> io::Result<Self> {
let raw_path = path.into();
let path = raw_path.canonicalize()?;
let only = only.into();
Ok(Self {
raw_path,
path,
recursive,
only,
})
}
pub fn raw_path(&self) -> &Path {
self.raw_path.as_path()
}
pub fn path(&self) -> &Path {
self.path.as_path()
}
/// Returns true if this watcher path applies to the given path.
/// This is accomplished by checking if the path is contained
/// within either the raw or canonicalized path of the watcher
/// and ensures that recursion rules are respected
pub fn applies_to_path(&self, path: &Path) -> bool {
let check_path = |path: &Path| -> bool {
let cnt = path.components().count();
// 0 means exact match from strip_prefix
// 1 means that it was within immediate directory (fine for non-recursive)
// 2+ means it needs to be recursive
cnt < 2 || self.recursive
};
match (
path.strip_prefix(self.path()),
path.strip_prefix(self.raw_path()),
) {
(Ok(p1), Ok(p2)) => check_path(p1) || check_path(p2),
(Ok(p), Err(_)) => check_path(p),
(Err(_), Ok(p)) => check_path(p),
(Err(_), Err(_)) => false,
}
}
} }
/// Holds information related to a spawned process on the server /// Holds information related to a spawned process on the server
@ -25,6 +141,27 @@ pub struct ProcessState {
} }
impl State { impl State {
pub fn map_paths_to_watcher_paths_and_replies<'a>(
&mut self,
paths: &'a [PathBuf],
) -> Vec<(Vec<&'a PathBuf>, &ChangeKindSet, &mut ReplyFn)> {
let mut results = Vec::new();
for (wp, reply) in self.watcher_paths.iter_mut() {
let mut wp_paths = Vec::new();
for path in paths {
if wp.applies_to_path(path) {
wp_paths.push(path);
}
}
if !wp_paths.is_empty() {
results.push((wp_paths, &wp.only, reply));
}
}
results
}
/// Pushes a new process associated with a connection /// Pushes a new process associated with a connection
pub fn push_process_state(&mut self, conn_id: usize, process_state: ProcessState) { pub fn push_process_state(&mut self, conn_id: usize, process_state: ProcessState) {
self.client_processes self.client_processes

@ -1,8 +0,0 @@
[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"]

@ -1,27 +0,0 @@
[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.7.3", 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"] }

@ -1,34 +0,0 @@
# 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

@ -1,74 +0,0 @@
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)
}

@ -1,41 +0,0 @@
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)
}

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

@ -1,17 +0,0 @@
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()
}

@ -1,41 +0,0 @@
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()
}

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

@ -1,79 +0,0 @@
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");
}

@ -1,79 +0,0 @@
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, text = $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_text_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, text = $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");
}

@ -1,210 +0,0 @@
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()));
}

@ -1,129 +0,0 @@
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");
}

@ -1,73 +0,0 @@
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());
}

@ -1,238 +0,0 @@
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());
}

@ -1,17 +0,0 @@
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 spawn_pty;
mod spawn_wait;
mod system_info;
mod write_file;
mod write_file_text;

@ -1,357 +0,0 @@
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());
}

@ -1,79 +0,0 @@
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());
}

@ -1,74 +0,0 @@
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());
}

@ -1,145 +0,0 @@
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());
}

@ -1,126 +0,0 @@
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");
}

@ -1,443 +0,0 @@
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: " .. tostring(err))
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: " .. tostring(err))
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: " .. tostring(err))
stdout = string.char(unpack(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: " .. tostring(err))
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 stderr: " .. tostring(err))
stderr = string.char(unpack(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();
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: " .. tostring(err))
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: " .. tostring(err))
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: " .. tostring(err))
})
.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: " .. tostring(err))
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: " .. tostring(err))
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: " .. tostring(err))
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: " .. tostring(err))
stdout = string.char(unpack(stdout))
assert(stdout == "some text\n", "Unexpected stdout received: " .. stdout)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -1,519 +0,0 @@
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, pty = { rows = 24, cols = 80 } }, 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, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
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, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
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: " .. tostring(err))
stdout = string.char(unpack(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_as_part_of_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_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, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
assert(proc, "Missing proc")
// Wait briefly to ensure the process sends stdout
$wait_fn()
// stderr is a broken pipe as it does not exist for pty
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(err)
// in a pty process, stderr is part of stdout
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: " .. tostring(err))
// stdout should match what we'd normally expect from stderr
stdout = string.char(unpack(stdout))
assert(stdout == "some stderr", "Unexpected stdout: " .. stdout)
})
.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, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
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, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
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: " .. tostring(err))
})
.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, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
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 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, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed spawning process: " .. tostring(err))
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: " .. tostring(err))
// Wait briefly to ensure that pty reflects everything
$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: " .. tostring(err))
// NOTE: We're removing whitespace as there's some issue with properly comparing
// due to something else being captured from pty
stdout = string.gsub(string.char(unpack(stdout)), "%s+", "")
// TODO: Sometimes this comes back as "sometextsometext" (double) and I'm assuming
// this is part of pty output, but the tests seem to have a race condition
// to produce it, so we're just checking for either right now
assert(
stdout == "sometext" or stdout == "sometextsometext",
"Unexpected stdout received: " .. 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_support_resizing_pty(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<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, proc
f(session, { cmd = $cmd, args = $args, pty = { rows = 24, cols = 80 } }, function(success, res)
if success then
proc = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed spawning process: " .. tostring(err))
assert(proc, "Missing proc")
local f = distant.utils.wrap_async(proc.resize_async, $schedule_fn)
local err
f(proc, { rows = 16, cols = 40 }, function(success, res)
if not success then
err = res
end
end)
assert(not err, "Unexpectedly failed resizing proc: " .. tostring(err))
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -1,173 +0,0 @@
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 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_wait_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_status_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_wait_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, output
f(session, { cmd = $cmd, args = $args }, function(success, res)
if success then
output = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
assert(output, "Missing process output")
assert(output.success, "Process output returned !success")
})
.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_capture_all_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 result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_wait_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, output
f(session, { cmd = $cmd, args = $args }, function(success, res)
if success then
output = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
assert(output, "Missing process output")
assert(output.stdout, "some 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_capture_all_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 result = lua
.load(chunk! {
local session = $new_session()
local distant = require("distant_lua")
local f = distant.utils.wrap_async(session.spawn_wait_async, $schedule_fn)
// Because of our scheduler, the invocation turns async -> sync
local err, output
f(session, { cmd = $cmd, args = $args }, function(success, res)
if success then
output = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed to spawn process: " .. tostring(err))
assert(output, "Missing process output")
assert(output.stderr, "some stderr")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -1,33 +0,0 @@
use crate::common::{fixtures::*, lua, poll, session};
use mlua::chunk;
use rstest::*;
#[rstest]
fn should_return_system_information(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 result = lua
.load(chunk! {
local session = $new_session()
local f = require("distant_lua").utils.wrap_async(
session.system_info_async,
$schedule_fn
)
// Because of our scheduler, the invocation turns async -> sync
local err, system_info
f(session, function(success, res)
if success then
system_info = res
else
err = res
end
end)
assert(not err, "Unexpectedly failed")
assert(system_info, "Missing system information")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -1,79 +0,0 @@
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");
}

@ -1,79 +0,0 @@
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, text = $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, text = $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");
}

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

@ -1,60 +0,0 @@
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");
}

@ -1,60 +0,0 @@
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,
text = $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_text_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,
text = $text
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we appended to the file
file.assert("line 1some text");
}

@ -1,161 +0,0 @@
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()));
}

@ -1,91 +0,0 @@
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");
}

@ -1,43 +0,0 @@
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());
}

@ -1,152 +0,0 @@
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());
}

@ -1,17 +0,0 @@
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 spawn_pty;
mod spawn_wait;
mod system_info;
mod write_file;
mod write_file_text;

@ -1,249 +0,0 @@
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());
}

@ -1,51 +0,0 @@
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());
}

@ -1,45 +0,0 @@
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());
}

@ -1,94 +0,0 @@
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());
}

@ -1,97 +0,0 @@
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");
}

@ -1,291 +0,0 @@
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()
stdout = string.char(unpack(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()
stderr = string.char(unpack(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()
stdout = string.char(unpack(stdout))
assert(stdout == "some text\n", "Unexpected stdin sent: " .. stdout)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -1,324 +0,0 @@
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,
pty = { rows = 24, cols = 80 }
})
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, pty = { rows = 24, cols = 80 } })
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, rows = 24, cols = 80 })
// Wait briefly to ensure the process sends stdout
$wait_fn()
local stdout = proc:read_stdout()
stdout = string.char(unpack(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, rows = 24, cols = 80 })
// Wait briefly to ensure the process sends stdout
$wait_fn()
local stderr = proc:read_stderr()
stderr = string.char(unpack(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, rows = 24, cols = 80 })
// 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, rows = 24, cols = 80 })
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, rows = 24, cols = 80 })
// 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, rows = 24, cols = 80 })
proc:write_stdin("some text\n")
// Wait briefly to ensure the process echoes stdin
$wait_fn()
local stdout = proc:read_stdout()
stdout = string.char(unpack(stdout))
assert(stdout == "some text\n", "Unexpected stdin sent: " .. 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_support_resizing_pty(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<String> = Vec::new();
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, rows = 24, cols = 80 })
// Wait briefly to ensure the process starts
$wait_fn()
proc:resize({ rows = 16, cols = 40 })
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -1,141 +0,0 @@
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 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_wait, 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 output = session:spawn_wait({ cmd = $cmd, args = $args })
assert(output, "Missing process output")
assert(output.success, "Process unexpectedly failed")
})
.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_capture_all_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 result = lua
.load(chunk! {
local session = $new_session()
local output = session:spawn_wait({ cmd = $cmd, args = $args })
assert(output, "Missing process output")
assert(output.success, "Process unexpectedly failed")
local stdout, stderr
stdout = string.char(unpack(output.stdout))
stderr = string.char(unpack(output.stderr))
assert(stdout == "some stdout", "Unexpected stdout: " .. stdout)
assert(stderr == "", "Unexpected stderr: " .. stderr)
})
.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_capture_all_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 result = lua
.load(chunk! {
local session = $new_session()
local output = session:spawn_wait({ cmd = $cmd, args = $args })
assert(output, "Missing process output")
assert(output.success, "Process unexpectedly failed")
local stdout, stderr
stdout = string.char(unpack(output.stdout))
stderr = string.char(unpack(output.stderr))
assert(stdout == "", "Unexpected stdout: " .. stdout)
assert(stderr == "some stderr", "Unexpected stderr: " .. stderr)
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -1,18 +0,0 @@
use crate::common::{fixtures::*, lua, session};
use mlua::chunk;
use rstest::*;
#[rstest]
fn should_return_system_info(ctx: &'_ DistantServerCtx) {
let lua = lua::make().unwrap();
let new_session = session::make_function(&lua, ctx).unwrap();
let result = lua
.load(chunk! {
local session = $new_session()
local system_info = session:system_info()
assert(system_info, "System info unexpectedly missing")
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
}

@ -1,60 +0,0 @@
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");
}

@ -1,60 +0,0 @@
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,
text = $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,
text = $text
})
})
.exec();
assert!(result.is_ok(), "Failed: {}", result.unwrap_err());
// Also verify that we appended to the file
file.assert("some text");
}

@ -1,17 +0,0 @@
[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",
]
[target.x86_64-unknown-linux-musl]
rustflags = [
"-C", "target-feature=-crt-static",
"-C", "linker=musl-cc",
]

@ -1,41 +0,0 @@
[package]
name = "distant-lua"
description = "Lua bindings to the distant Rust crates"
categories = ["api-bindings", "network-programming"]
keywords = ["api", "async"]
version = "0.16.0"
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.16.0", path = "../distant-core" }
distant-ssh2 = { version = "=0.16.0", features = ["serde"], path = "../distant-ssh2" }
futures = "0.3.17"
log = "0.4.14"
mlua = { version = "0.7.3", 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"

@ -1,105 +0,0 @@
# 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, *.dll for Windows)
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 at
the *root*. The library cannot be within any submodule otherwise it fails
to load appropriate symbols. For neovim, this means directly within the
`lua/` directory.
```lua
local distant = require("distant_lua")
-- The majority of the distant lua module provides async and sync variants
-- of methods; however, launching a session is currently only synchronous
local session = distant.session.launch({ host = "127.0.0.1" })
-- Sync methods are executed in a blocking fashion, returning the result of
-- the operation if successful or throwing an error if failing. Use `pcall`
-- if you want to capture the error
local success, result = pcall(session.read_dir, session, { path = "path/to/dir" })
if success then
for _, entry in ipairs(result.entries) do
print("Entry", entry.file_type, entry.path, entry.depth)
end
else
print(result)
end
-- Async methods have _async as a suffix and need to be polled from
-- Lua in some manner; the `wrap_async` function provides a convience
-- to do so taking an async distant function and a scheduling function
local schedule_fn = function(cb) end
local read_dir = distant.utils.wrap_async(session.read_dir_async, schedule_fn)
read_dir(session, { path = "path/to/dir" }, function(success, result)
-- success: Returns true if ok and false if err
-- result: If success is true, then is the resulting value,
-- otherwise is the error
print("Success", success)
if success then
for _, entry in ipairs(result.entries) do
print("Entry", entry.file_type, entry.path, entry.depth)
end
else
print(result)
end
end)
-- For neovim, there exists a helper function that converts async functions
-- into functions that take callbacks, executing the asynchronous logic
-- using neovim's event loop powered by libuv
local read_dir = distant.utils.nvim_wrap_async(session.read_dir_async)
read_dir(session, { path = "path/to/dir" }, function(success, result)
-- success: Returns true if ok and false if err
-- result: If success is true, then is the resulting value,
-- otherwise is the error
print("Success", success)
if success then
for _, entry in ipairs(result.entries) do
print("Entry", entry.file_type, entry.path, entry.depth)
end
else
print(result)
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

@ -1,8 +0,0 @@
/// Default timeout (15 secs)
pub const TIMEOUT_MILLIS: u64 = 15000;
/// Default polling interval for internal process reading and writing
pub const PROC_POLL_TIMEOUT: u64 = 200;
/// Default polling interval for neovim (0.2 secs)
pub const NVIM_POLL_TIMEOUT: u64 = 200;

@ -1,58 +0,0 @@
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 constants;
mod log;
mod runtime;
mod session;
mod utils;
#[mlua::lua_module]
fn distant_lua(lua: &Lua) -> LuaResult<LuaTable> {
let exports = lua.create_table()?;
// Provide a static pending type used when consumer wants to use async functions
// directly without wrapping them with a scheduler
exports.set("pending", utils::pending(lua)?)?;
// API modules available for users
exports.set("log", log::make_log_tbl(lua)?)?;
exports.set("session", session::make_session_tbl(lua)?)?;
exports.set("utils", utils::make_utils_tbl(lua)?)?;
exports.set("version", make_version_tbl(lua)?)?;
Ok(exports)
}
macro_rules! set_nonempty_env {
($tbl:ident, $key:literal, $env_key:literal) => {{
let value = env!($env_key);
if !value.is_empty() {
$tbl.set($key, value)?;
}
}};
}
fn make_version_tbl(lua: &Lua) -> LuaResult<LuaTable> {
let tbl = lua.create_table()?;
set_nonempty_env!(tbl, "full", "CARGO_PKG_VERSION");
set_nonempty_env!(tbl, "major", "CARGO_PKG_VERSION_MAJOR");
set_nonempty_env!(tbl, "minor", "CARGO_PKG_VERSION_MINOR");
set_nonempty_env!(tbl, "patch", "CARGO_PKG_VERSION_PATCH");
set_nonempty_env!(tbl, "pre", "CARGO_PKG_VERSION_PRE");
Ok(tbl)
}

@ -1,114 +0,0 @@
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)
}

@ -1,38 +0,0 @@
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(),
})
})
}

@ -1,330 +0,0 @@
use crate::{runtime, utils};
use distant_core::{
SecretKey32, Session as DistantSession, SessionChannel, SessionDetails, XChaCha20Poly1305Codec,
};
use distant_ssh2::{IntoDistantSessionOpts, Ssh2Session};
use log::*;
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_all() -> Vec<Session>
tbl.set("get_all", lua.create_function(|_, ()| Session::all())?)?;
// 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 with_session<T>(id: usize, f: impl FnOnce(&DistantSession) -> T) -> LuaResult<T> {
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(f(session))
}
fn get_session_details(id: usize) -> LuaResult<Option<SessionDetails>> {
with_session(id, |session| session.details().cloned())
}
fn get_session_channel(id: usize) -> LuaResult<SessionChannel> {
with_session(id, |session| 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 }
}
/// Retrieves all sessions
pub fn all() -> LuaResult<Vec<Self>> {
Ok(SESSION_MAP
.read()
.map_err(|x| x.to_string().to_lua_err())?
.keys()
.copied()
.map(Self::new)
.collect())
}
/// Launches a new distant session on a remote machine
pub async fn launch(opts: LaunchOpts<'_>) -> LuaResult<Self> {
trace!("Session::launch({:?})", opts);
let LaunchOpts {
host,
mode,
handler,
ssh,
distant,
timeout,
} = opts;
// First, establish a connection to an SSH server
debug!("Connecting to {} {:#?}", host, ssh);
let mut ssh_session = Ssh2Session::connect(host.as_str(), ssh).to_lua_err()?;
// Second, authenticate with the server
debug!("Authenticating against {}", host);
ssh_session.authenticate(handler).await.to_lua_err()?;
// Third, convert our ssh session into a distant session based on desired method
debug!("Mapping session for {} into {:?}", host, mode);
let session = match mode {
Mode::Distant => ssh_session
.into_distant_session(IntoDistantSessionOpts {
binary: distant.bin,
args: distant.args,
use_login_shell: distant.use_login_shell,
timeout,
})
.await
.to_lua_err()?,
Mode::Ssh => ssh_session.into_ssh_client_session().await.to_lua_err()?,
};
// Fourth, store our current session in our global map and then return a reference
let id = utils::rand_u32()? as usize;
debug!("Session {} established against {}", id, host);
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> {
trace!("Session::connect({:?})", opts);
debug!("Looking up {}", opts.host);
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()?;
debug!("Constructing codec");
let key: SecretKey32 = opts.key.parse().to_lua_err()?;
let codec = XChaCha20Poly1305Codec::from(key);
debug!("Connecting to {}", addr);
let session = DistantSession::tcp_connect_timeout(addr, codec, opts.timeout)
.await
.to_lua_err()?;
let id = utils::rand_u32()? as usize;
debug!("Session {} established against {}", id, opts.host);
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: Option<LuaValue>| {
let params: LuaValue = match params {
Some(params) => params,
None => LuaValue::Table($lua.create_table()?),
};
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: Option<LuaValue>| async move {
let rt = crate::runtime::get_runtime()?;
let params: LuaValue = match params {
Some(params) => params,
None => LuaValue::Table($lua.create_table()?),
};
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));
fields.add_field_method_get("details", |lua, this| {
get_session_details(this.id).and_then(|x| to_value!(lua, &x))
});
}
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("is_active", |_, this, ()| {
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_wait);
impl_methods!(methods, spawn_lsp, |_lua, proc| {
Ok(RemoteLspProcess::from_distant(proc))
});
impl_methods!(methods, system_info, |lua, info| { lua.to_value(&info) });
impl_methods!(methods, write_file);
impl_methods!(methods, write_file_text);
}
}

@ -1,215 +0,0 @@
use crate::{
runtime,
session::proc::{Output, RemoteProcess as LuaRemoteProcess},
};
use distant_core::{
data::PtySize, DirEntry, Error as Failure, Metadata, RemoteLspProcess, RemoteProcess,
SessionChannel, SessionChannelExt, SystemInfo,
};
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, text: String }, |channel, tenant, params| {
channel.append_file_text(tenant, params.path, params.text).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, #[serde(default)] args: Vec<String>, #[serde(default)] pty: Option<PtySize>, #[serde(default)] persist: bool },
|channel, tenant, params| {
channel.spawn(tenant, params.cmd, params.args, params.persist, params.pty).await
}
);
make_api!(
spawn_wait,
Output,
{ cmd: String, #[serde(default)] args: Vec<String>, #[serde(default)] pty: Option<PtySize>, #[serde(default)] persist: bool },
|channel, tenant, params| {
let proc = channel.spawn(
tenant,
params.cmd,
params.args,
params.persist,
params.pty,
).await.to_lua_err()?;
let id = LuaRemoteProcess::from_distant_async(proc).await?.id;
LuaRemoteProcess::output_async(id).await
}
);
make_api!(
spawn_lsp,
RemoteLspProcess,
{ cmd: String, #[serde(default)] args: Vec<String>, #[serde(default)] pty: Option<PtySize>, #[serde(default)] persist: bool },
|channel, tenant, params| {
channel.spawn_lsp(tenant, params.cmd, params.args, params.persist, params.pty).await
}
);
make_api!(system_info, SystemInfo, |channel, tenant, _params| {
channel.system_info(tenant).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, text: String },
|channel, tenant, params| { channel.write_file_text(tenant, params.path, params.text).await }
);

@ -1,374 +0,0 @@
use crate::constants::TIMEOUT_MILLIS;
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: Option<u64> = tbl.get("timeout")?;
Duration::from_millis(milliseconds.unwrap_or(TIMEOUT_MILLIS))
},
}),
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> {
/// Host to connect to remotely (e.g. example.com)
pub host: String,
/// Mode to use for communication (ssh or distant server)
pub mode: Mode,
/// Callbacks to be triggered on various authentication events
pub handler: Ssh2AuthHandler<'a>,
/// Miscellaneous ssh configuration options
pub ssh: Ssh2SessionOpts,
/// Options specific to launching the distant binary on the remote machine
pub distant: LaunchDistantOpts,
/// Maximum time to wait for launch to complete
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("distant", &self.distant)
.field("timeout", &self.timeout)
.finish()
}
}
impl<'lua> FromLua<'lua> for LaunchOpts<'lua> {
fn from_lua(lua_value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
let Ssh2AuthHandler {
on_authenticate,
on_banner,
on_error,
on_host_verify,
} = Default::default();
match lua_value {
LuaValue::Table(tbl) => Ok(Self {
host: tbl.get("host")?,
mode: {
let mode: Option<LuaValue> = tbl.get("mode")?;
match mode {
Some(value) => lua.from_value(value)?,
None => Default::default(),
}
},
handler: Ssh2AuthHandler {
on_authenticate: {
let f: Option<LuaFunction> = tbl.get("on_authenticate")?;
match f {
Some(f) => 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))
}),
None => on_authenticate,
}
},
on_banner: {
let f: Option<LuaFunction> = tbl.get("on_banner")?;
match f {
Some(f) => Box::new(move |banner| {
let _ = f.call::<String, ()>(banner.to_string());
}),
None => on_banner,
}
},
on_host_verify: {
let f: Option<LuaFunction> = tbl.get("on_host_verify")?;
match f {
Some(f) => Box::new(move |host| {
f.call::<String, bool>(host.to_string())
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))
}),
None => on_host_verify,
}
},
on_error: {
let f: Option<LuaFunction> = tbl.get("on_error")?;
match f {
Some(f) => Box::new(move |err| {
let _ = f.call::<String, ()>(err.to_string());
}),
None => on_error,
}
},
},
ssh: {
let ssh_tbl: Option<LuaValue> = tbl.get("ssh")?;
match ssh_tbl {
Some(value) => lua.from_value(value)?,
None => Default::default(),
}
},
distant: {
let distant_tbl: Option<LuaValue> = tbl.get("distant")?;
match distant_tbl {
Some(value) => LaunchDistantOpts::from_lua(value, lua)?,
None => Default::default(),
}
},
timeout: {
let milliseconds: Option<u64> = tbl.get("timeout")?;
Duration::from_millis(milliseconds.unwrap_or(TIMEOUT_MILLIS))
},
}),
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(Debug)]
pub struct LaunchDistantOpts {
/// Binary representing the distant server on the remote machine
pub bin: String,
/// Additional CLI options to pass to the distant server when starting
pub args: String,
/// If true, will run distant via `echo <CMD> | $SHELL -l`, which will spawn a login shell to
/// execute distant
pub use_login_shell: bool,
}
impl Default for LaunchDistantOpts {
/// Create default options
///
/// * bin = "distant"
/// * args = ""
/// * use_login_shell = false
fn default() -> Self {
Self {
bin: String::from("distant"),
args: String::new(),
use_login_shell: false,
}
}
}
impl<'lua> FromLua<'lua> for LaunchDistantOpts {
fn from_lua(lua_value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
let LaunchDistantOpts {
bin: default_bin,
args: default_args,
use_login_shell: default_use_login_shell,
} = Default::default();
match lua_value {
LuaValue::Table(tbl) => Ok(Self {
bin: {
let bin: Option<String> = tbl.get("bin")?;
bin.unwrap_or(default_bin)
},
// Allows "--some --args" or {"--some", "--args"}
args: {
let value: LuaValue = tbl.get("args")?;
match value {
LuaValue::Nil => default_args,
LuaValue::String(args) => args.to_str()?.to_string(),
x => {
let args: Vec<String> = lua.from_value(x)?;
args.join(" ")
}
}
},
use_login_shell: tbl
.get::<_, Option<bool>>("use_login_shell")?
.unwrap_or(default_use_login_shell),
}),
LuaValue::Nil => Err(LuaError::FromLuaConversionError {
from: "Nil",
to: "LaunchDistantOpts",
message: None,
}),
LuaValue::Boolean(_) => Err(LuaError::FromLuaConversionError {
from: "Boolean",
to: "LaunchDistantOpts",
message: None,
}),
LuaValue::LightUserData(_) => Err(LuaError::FromLuaConversionError {
from: "LightUserData",
to: "LaunchDistantOpts",
message: None,
}),
LuaValue::Integer(_) => Err(LuaError::FromLuaConversionError {
from: "Integer",
to: "LaunchDistantOpts",
message: None,
}),
LuaValue::Number(_) => Err(LuaError::FromLuaConversionError {
from: "Number",
to: "LaunchDistantOpts",
message: None,
}),
LuaValue::String(_) => Err(LuaError::FromLuaConversionError {
from: "String",
to: "LaunchDistantOpts",
message: None,
}),
LuaValue::Function(_) => Err(LuaError::FromLuaConversionError {
from: "Function",
to: "LaunchDistantOpts",
message: None,
}),
LuaValue::Thread(_) => Err(LuaError::FromLuaConversionError {
from: "Thread",
to: "LaunchDistantOpts",
message: None,
}),
LuaValue::UserData(_) => Err(LuaError::FromLuaConversionError {
from: "UserData",
to: "LaunchDistantOpts",
message: None,
}),
LuaValue::Error(_) => Err(LuaError::FromLuaConversionError {
from: "Error",
to: "LaunchDistantOpts",
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
}
}

@ -1,392 +0,0 @@
use crate::{constants::PROC_POLL_TIMEOUT, runtime};
use distant_core::{
data::PtySize, RemoteLspProcess as DistantRemoteLspProcess,
RemoteProcess as DistantRemoteProcess,
};
use mlua::{prelude::*, UserData, UserDataFields, UserDataMethods};
use once_cell::sync::Lazy;
use std::{collections::HashMap, io, time::Duration};
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 {
pub(crate) id: usize,
}
impl $name {
pub fn new(id: usize) -> Self {
Self { id }
}
pub fn from_distant(proc: $type) -> LuaResult<Self> {
runtime::get_runtime()?.block_on(Self::from_distant_async(proc))
}
pub async fn from_distant_async(proc: $type) -> LuaResult<Self> {
let id = proc.id();
$map_name.write().await.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<()> {
// NOTE: We must spawn a task that continually tries to send stdin as
// if we wait until successful then we hold the lock the entire time
runtime::spawn(async move {
loop {
let is_done = with_proc_async!($map_name, id, proc -> {
let res = proc.stdin
.as_mut()
.ok_or_else(|| {
io::Error::new(io::ErrorKind::BrokenPipe, "Stdin closed").to_lua_err()
})?
.try_write(data.as_bytes());
match res {
Ok(_) => Ok(true),
Err(x) if x.kind() == io::ErrorKind::WouldBlock => Ok(false),
Err(x) => Err(x),
}
})?;
if is_done {
break;
}
tokio::time::sleep(Duration::from_millis(PROC_POLL_TIMEOUT)).await;
}
Ok(())
}).await
}
fn close_stdin(id: usize) -> LuaResult<()> {
with_proc!($map_name, id, proc -> {
let _ = proc.stdin.take();
Ok(())
})
}
fn read_stdout(id: usize) -> LuaResult<Option<Vec<u8>>> {
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<Vec<u8>> {
// NOTE: We must spawn a task that continually tries to read stdout as
// if we wait until successful then we hold the lock the entire time
runtime::spawn(async move {
loop {
let data = 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()
})?
.try_read()
.to_lua_err()?
});
if let Some(data) = data {
break Ok(data);
}
}
}).await
}
fn read_stderr(id: usize) -> LuaResult<Option<Vec<u8>>> {
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<Vec<u8>> {
// NOTE: We must spawn a task that continually tries to read stdout as
// if we wait until successful then we hold the lock the entire time
runtime::spawn(async move {
loop {
let data = 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()
})?
.try_read()
.to_lua_err()?
});
if let Some(data) = data {
break Ok(data);
}
}
}).await
}
fn resize(id: usize, size: PtySize) -> LuaResult<()> {
runtime::block_on(Self::resize_async(id, size))
}
async fn resize_async(id: usize, size: PtySize) -> LuaResult<()> {
with_proc_async!($map_name, id, proc -> {
proc.resize(size).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 status(id: usize) -> LuaResult<Option<Status>> {
runtime::block_on(Self::status_async(id))
}
async fn status_async(id: usize) -> LuaResult<Option<Status>> {
let lock = $map_name.read().await;
let proc = lock.get(&id).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("No remote process found with id {}", id),
)
.to_lua_err()
})?;
Ok(proc.status().await.map(|(success, exit_code)| Status {
success,
exit_code,
}))
}
fn wait(id: usize) -> LuaResult<(bool, Option<i32>)> {
runtime::block_on(Self::wait_async(id))
}
async fn wait_async(id: usize) -> LuaResult<(bool, Option<i32>)> {
let proc = $map_name.write().await.remove(&id).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("No remote process found with id {}", id),
)
.to_lua_err()
})?;
proc.wait().await.to_lua_err()
}
fn output(id: usize) -> LuaResult<Output> {
runtime::block_on(Self::output_async(id))
}
pub(crate) async fn output_async(id: usize) -> LuaResult<Output> {
let mut proc = $map_name.write().await.remove(&id).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("No remote process found with id {}", id),
)
.to_lua_err()
})?;
// Remove the stdout and stderr streams before letting process run to completion
let mut stdout = proc.stdout.take().unwrap();
let mut stderr = proc.stderr.take().unwrap();
// Gather stdout and stderr after process completes
let (success, exit_code) = proc.wait().await.to_lua_err()?;
let mut stdout_buf = Vec::new();
while let Ok(Some(data)) = stdout.try_read() {
stdout_buf.extend(data);
}
let mut stderr_buf = Vec::new();
while let Ok(Some(data)) = stderr.try_read() {
stderr_buf.extend(data);
}
Ok(Output {
success,
exit_code,
stdout: stdout_buf,
stderr: stderr_buf,
})
}
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("status", |_, this, ()| Self::status(this.id));
methods.add_async_method("status_async", |_, this, ()| {
runtime::spawn(Self::status_async(this.id))
});
methods.add_method("wait", |_, this, ()| Self::wait(this.id));
methods.add_async_method("wait_async", |_, this, ()| {
runtime::spawn(Self::wait_async(this.id))
});
methods.add_method("output", |_, this, ()| Self::output(this.id));
methods.add_async_method("output_async", |_, this, ()| {
runtime::spawn(Self::output_async(this.id))
});
methods.add_method("resize", |lua, this, value: LuaValue| {
let size: PtySize = lua.from_value(value)?;
Self::resize(this.id, size)
});
methods.add_async_method("resize_async", |lua, this, value: LuaValue| {
let size: LuaResult<PtySize> = lua.from_value(value);
runtime::spawn(async move {
let size = size?;
Self::resize_async(this.id, size).await
})
});
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));
}
}
};
}
/// Represents process status
#[derive(Clone, Debug)]
pub struct Status {
pub success: bool,
pub exit_code: Option<i32>,
}
impl UserData for Status {
fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("success", |_, this| Ok(this.success));
fields.add_field_method_get("exit_code", |_, this| Ok(this.exit_code));
}
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("to_tbl", |lua, this, ()| {
let tbl = lua.create_table()?;
tbl.set("success", this.success)?;
tbl.set("exit_code", this.exit_code)?;
Ok(tbl)
});
}
}
/// Represents process output
#[derive(Clone, Debug)]
pub struct Output {
pub success: bool,
pub exit_code: Option<i32>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
impl UserData for Output {
fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("success", |_, this| Ok(this.success));
fields.add_field_method_get("exit_code", |_, this| Ok(this.exit_code));
fields.add_field_method_get("stdout", |_, this| Ok(this.stdout.to_vec()));
fields.add_field_method_get("stderr", |_, this| Ok(this.stderr.to_vec()));
}
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("to_tbl", |lua, this, ()| {
let tbl = lua.create_table()?;
tbl.set("success", this.success)?;
tbl.set("exit_code", this.exit_code)?;
tbl.set("stdout", this.stdout.to_vec())?;
tbl.set("stderr", this.stdout.to_vec())?;
Ok(tbl)
});
}
}
impl_process!(RemoteProcess, DistantRemoteProcess, PROC_MAP);
impl_process!(RemoteLspProcess, DistantRemoteLspProcess, LSP_PROC_MAP);

@ -1,126 +0,0 @@
use crate::constants::NVIM_POLL_TIMEOUT;
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, millis): (LuaFunction, Option<u64>)| {
nvim_wrap_async(lua, async_fn, millis.unwrap_or(NVIM_POLL_TIMEOUT))
})?,
)?;
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.defer_fn` from neovim
pub fn nvim_wrap_async<'a>(
lua: &'a Lua,
async_fn: LuaFunction<'a>,
millis: u64,
) -> LuaResult<LuaFunction<'a>> {
let schedule_fn = lua
.load(chunk! {
function(cb)
return vim.defer_fn(cb, $millis)
end
})
.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())
}

@ -49,6 +49,15 @@ struct Outgoing {
post_hook: Option<PostHook>, post_hook: Option<PostHook>,
} }
impl Outgoing {
pub fn unsupported() -> Self {
Self::from(ResponseData::from(io::Error::new(
io::ErrorKind::Other,
"Unsupported",
)))
}
}
impl From<ResponseData> for Outgoing { impl From<ResponseData> for Outgoing {
fn from(data: ResponseData) -> Self { fn from(data: ResponseData) -> Self {
Self { Self {
@ -88,6 +97,8 @@ pub(super) async fn process(
RequestData::Remove { path, force } => remove(session, path, force).await, RequestData::Remove { path, force } => remove(session, path, force).await,
RequestData::Copy { src, dst } => copy(session, src, dst).await, RequestData::Copy { src, dst } => copy(session, src, dst).await,
RequestData::Rename { src, dst } => rename(session, src, dst).await, RequestData::Rename { src, dst } => rename(session, src, dst).await,
RequestData::Watch { .. } => Ok(Outgoing::unsupported()),
RequestData::Unwatch { .. } => Ok(Outgoing::unsupported()),
RequestData::Exists { path } => exists(session, path).await, RequestData::Exists { path } => exists(session, path).await,
RequestData::Metadata { RequestData::Metadata {
path, path,

@ -1894,3 +1894,50 @@ async fn system_info_should_send_system_info_based_on_binary(#[future] session:
res.payload[0] res.payload[0]
); );
} }
#[rstest]
#[tokio::test]
async fn watch_should_fail_as_unsupported(#[future] session: Session) {
let mut session = session.await;
let req = Request::new(
"test-tenant",
vec![RequestData::Watch {
path: PathBuf::from("/some/path"),
recursive: true,
only: Default::default(),
except: Default::default(),
}],
);
let res = session.send(req).await.unwrap();
assert_eq!(res.payload.len(), 1, "Wrong payload size");
match &res.payload[0] {
ResponseData::Error(x) => {
assert_eq!(x.to_string(), "Other: Unsupported");
}
x => panic!("Unexpected response: {:?}", x),
}
}
#[rstest]
#[tokio::test]
async fn unwatch_should_fail_as_unsupported(#[future] session: Session) {
let mut session = session.await;
let req = Request::new(
"test-tenant",
vec![RequestData::Unwatch {
path: PathBuf::from("/some/path"),
}],
);
let res = session.send(req).await.unwrap();
assert_eq!(res.payload.len(), 1, "Wrong payload size");
match &res.payload[0] {
ResponseData::Error(x) => {
assert_eq!(x.to_string(), "Other: Unsupported");
}
x => panic!("Unexpected response: {:?}", x),
}
}

@ -1,4 +1,4 @@
use distant_core::{RemoteProcessError, TransportError}; use distant_core::{RemoteProcessError, SessionChannelExtError, TransportError, WatchError};
/// Exit codes following https://www.freebsd.org/cgi/man.cgi?query=sysexits&sektion=3 /// Exit codes following https://www.freebsd.org/cgi/man.cgi?query=sysexits&sektion=3
#[derive(Copy, Clone, PartialEq, Eq, Hash)] #[derive(Copy, Clone, PartialEq, Eq, Hash)]
@ -119,6 +119,28 @@ impl ExitCodeError for RemoteProcessError {
} }
} }
impl ExitCodeError for SessionChannelExtError {
fn to_exit_code(&self) -> ExitCode {
match self {
Self::Failure(_) => ExitCode::Software,
Self::TransportError(x) => x.to_exit_code(),
Self::MismatchedResponse => ExitCode::Protocol,
}
}
}
impl ExitCodeError for WatchError {
fn to_exit_code(&self) -> ExitCode {
match self {
Self::MissingConfirmation => ExitCode::Protocol,
Self::ServerError(_) => ExitCode::Software,
Self::TransportError(x) => x.to_exit_code(),
Self::QueuedChangeDropped => ExitCode::Software,
Self::UnexpectedResponse(_) => ExitCode::Protocol,
}
}
}
impl<T: ExitCodeError + 'static> From<T> for Box<dyn ExitCodeError> { impl<T: ExitCodeError + 'static> From<T> for Box<dyn ExitCodeError> {
fn from(x: T) -> Self { fn from(x: T) -> Self {
Box::new(x) Box::new(x)

@ -1,6 +1,6 @@
use crate::opt::Format; use crate::opt::Format;
use distant_core::{ use distant_core::{
data::{Error, Metadata, SystemInfo}, data::{ChangeKind, Error, Metadata, SystemInfo},
Response, ResponseData, Response, ResponseData,
}; };
use log::*; use log::*;
@ -127,6 +127,26 @@ fn format_shell(data: ResponseData) -> ResponseOut {
.join("\n") .join("\n")
.into_bytes(), .into_bytes(),
), ),
ResponseData::Changed(change) => ResponseOut::StdoutLine(
format!(
"{}{}",
match change.kind {
ChangeKind::Create => "Following paths were created:\n",
ChangeKind::Remove => "Following paths were removed:\n",
x if x.is_access_kind() => "Following paths were accessed:\n",
x if x.is_modify_kind() => "Following paths were modified:\n",
x if x.is_rename_kind() => "Following paths were renamed:\n",
_ => "Following paths were affected:\n",
},
change
.paths
.into_iter()
.map(|p| format!("* {}", p.to_string_lossy()))
.collect::<Vec<String>>()
.join("\n")
)
.into_bytes(),
),
ResponseData::Exists { value: exists } => { ResponseData::Exists { value: exists } => {
if exists { if exists {
ResponseOut::StdoutLine(b"true".to_vec()) ResponseOut::StdoutLine(b"true".to_vec())

@ -9,7 +9,8 @@ use crate::{
}; };
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use distant_core::{ use distant_core::{
LspData, RemoteProcess, RemoteProcessError, Request, RequestData, Session, TransportError, ChangeKindSet, LspData, RemoteProcess, RemoteProcessError, Request, RequestData, Response,
ResponseData, Session, TransportError, WatchError, Watcher,
}; };
use tokio::{io, time::Duration}; use tokio::{io, time::Duration};
@ -23,6 +24,7 @@ pub enum Error {
OperationFailed, OperationFailed,
RemoteProcess(RemoteProcessError), RemoteProcess(RemoteProcessError),
Transport(TransportError), Transport(TransportError),
Watch(WatchError),
} }
impl ExitCodeError for Error { impl ExitCodeError for Error {
@ -42,6 +44,7 @@ impl ExitCodeError for Error {
Self::OperationFailed => ExitCode::Software, Self::OperationFailed => ExitCode::Software,
Self::RemoteProcess(x) => x.to_exit_code(), Self::RemoteProcess(x) => x.to_exit_code(),
Self::Transport(x) => x.to_exit_code(), Self::Transport(x) => x.to_exit_code(),
Self::Watch(x) => x.to_exit_code(),
} }
} }
} }
@ -84,7 +87,38 @@ async fn start(
let is_shell_format = matches!(cmd.format, Format::Shell); let is_shell_format = matches!(cmd.format, Format::Shell);
match (cmd.interactive, cmd.operation) { match (cmd.interactive, cmd.operation) {
// ProcRun request w/ shell format is specially handled and we ignore interactive as // Watch request w/ shell format is specially handled and we ignore interactive as
// watch will run and wait
(
_,
Some(RequestData::Watch {
path,
recursive,
only,
except,
}),
) if is_shell_format => {
let mut watcher = Watcher::watch(
utils::new_tenant(),
session.into_channel(),
path,
recursive,
only.into_iter().collect::<ChangeKindSet>(),
except.into_iter().collect::<ChangeKindSet>(),
)
.await?;
// Continue to receive and process changes
while let Some(change) = watcher.next().await {
// TODO: Provide a cleaner way to print just a change
let res = Response::new("", 0, vec![ResponseData::Changed(change)]);
ResponseOut::new(cmd.format, res)?.print()
}
Ok(())
}
// ProcSpawn request w/ shell format is specially handled and we ignore interactive as
// the stdin will be used for sending ProcStdin to remote process // the stdin will be used for sending ProcStdin to remote process
( (
_, _,

@ -13,3 +13,4 @@ mod proc_spawn;
mod remove; mod remove;
mod rename; mod rename;
mod system_info; mod system_info;
mod watch;

@ -0,0 +1,526 @@
use crate::cli::{fixtures::*, utils::random_tenant};
use assert_fs::prelude::*;
use distant_core::{data::ErrorKind, Request, RequestData, Response, ResponseData};
use rstest::*;
use std::{
io,
io::{BufRead, BufReader, Read, Write},
path::PathBuf,
process::Command,
sync::mpsc,
thread,
time::{Duration, Instant},
};
fn wait_a_bit() {
wait_millis(250);
}
fn wait_even_longer() {
wait_millis(500);
}
fn wait_millis(millis: u64) {
thread::sleep(Duration::from_millis(millis));
}
struct ThreadedReader {
#[allow(dead_code)]
handle: thread::JoinHandle<io::Result<()>>,
rx: mpsc::Receiver<String>,
}
impl ThreadedReader {
pub fn new<R>(reader: R) -> Self
where
R: Read + Send + 'static,
{
let (tx, rx) = mpsc::channel();
let handle = thread::spawn(move || {
let mut reader = BufReader::new(reader);
let mut line = String::new();
loop {
match reader.read_line(&mut line) {
Ok(0) => break Ok(()),
Ok(_) => {
// Consume the line and create an empty line to
// be filled in next time
let line2 = line;
line = String::new();
if let Err(line) = tx.send(line2) {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"Failed to pass along line because channel closed! Line: '{}'",
line.0
),
));
}
}
Err(x) => return Err(x),
}
}
});
Self { handle, rx }
}
/// Tries to read the next line if available
pub fn try_read_line(&mut self) -> Option<String> {
self.rx.try_recv().ok()
}
/// Reads the next line, waiting for at minimum "timeout"
pub fn try_read_line_timeout(&mut self, timeout: Duration) -> Option<String> {
let start_time = Instant::now();
let mut checked_at_least_once = false;
while !checked_at_least_once || start_time.elapsed() < timeout {
if let Some(line) = self.try_read_line() {
return Some(line);
}
checked_at_least_once = true;
}
None
}
/// Reads the next line, waiting for at minimum "timeout" before panicking
pub fn read_line_timeout(&mut self, timeout: Duration) -> String {
let start_time = Instant::now();
let mut checked_at_least_once = false;
while !checked_at_least_once || start_time.elapsed() < timeout {
if let Some(line) = self.try_read_line() {
return line;
}
checked_at_least_once = true;
}
panic!("Reached timeout of {:?}", timeout);
}
/// Reads the next line, waiting for at minimum default timeout before panicking
#[allow(dead_code)]
pub fn read_line_default_timeout(&mut self) -> String {
self.read_line_timeout(Self::default_timeout())
}
/// Tries to read the next response if available
///
/// Will panic if next line is not a valid response
#[allow(dead_code)]
pub fn try_read_response(&mut self) -> Option<Response> {
self.try_read_line().map(|line| {
serde_json::from_str(&line)
.unwrap_or_else(|_| panic!("Invalid response format for {}", line))
})
}
/// Reads the next response, waiting for at minimum "timeout" before panicking
pub fn read_response_timeout(&mut self, timeout: Duration) -> Response {
let line = self.read_line_timeout(timeout);
serde_json::from_str(&line)
.unwrap_or_else(|_| panic!("Invalid response format for {}", line))
}
/// Reads the next response, waiting for at minimum default timeout before panicking
pub fn read_response_default_timeout(&mut self) -> Response {
self.read_response_timeout(Self::default_timeout())
}
/// Creates a new duration representing a default timeout for the threaded reader
pub fn default_timeout() -> Duration {
Duration::from_millis(250)
}
/// Waits for reader to shut down, returning the result
#[allow(dead_code)]
pub fn wait(self) -> io::Result<()> {
match self.handle.join() {
Ok(x) => x,
Err(x) => std::panic::resume_unwind(x),
}
}
}
fn send_watch_request<W>(
writer: &mut W,
reader: &mut ThreadedReader,
path: impl Into<PathBuf>,
recursive: bool,
) -> Response
where
W: Write,
{
let req = Request {
id: rand::random(),
tenant: random_tenant(),
payload: vec![RequestData::Watch {
path: path.into(),
recursive,
only: Vec::new(),
except: Vec::new(),
}],
};
// Send our request to the process
let msg = format!("{}\n", serde_json::to_string(&req).unwrap());
writer
.write_all(msg.as_bytes())
.expect("Failed to write to process");
// Pause a bit to ensure that the process started and processed our request
wait_a_bit();
// Ensure we got an acknowledgement of watching
let res = reader.read_response_default_timeout();
assert_eq!(res.payload[0], ResponseData::Ok);
res
}
// TODO: For some reason, this always fails on linux, so we're skipping the test
// for that platform right now.
#[rstest]
#[cfg_attr(linux, ignore)]
fn should_support_watching_a_single_file(mut action_std_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.touch().unwrap();
// distant action watch {path}
let mut child = action_std_cmd
.args(&["watch", file.to_str().unwrap()])
.spawn()
.expect("Failed to execute");
// Wait for the process to be ready
wait_a_bit();
// Manipulate the file
file.write_str("some text").unwrap();
// Pause a bit to ensure that the change is detected and reported
wait_even_longer();
let mut stdout = ThreadedReader::new(child.stdout.take().unwrap());
let mut stdout_data = String::new();
while let Some(line) = stdout.try_read_line_timeout(ThreadedReader::default_timeout()) {
stdout_data.push_str(&line);
}
// Close out the process and collect the output
let _ = child.kill().expect("Failed to terminate process");
let output = child.wait_with_output().expect("Failed to wait for output");
let stderr_data = String::from_utf8_lossy(&output.stderr).to_string();
let path = file
.to_path_buf()
.canonicalize()
.unwrap()
.to_str()
.unwrap()
.to_string();
// Verify we get information printed out about the change
assert!(
stdout_data.contains(&path),
"\"{}\" missing {}",
stdout_data,
path
);
assert_eq!(stderr_data, "");
}
// TODO: For some reason, this always fails on linux, so we're skipping the test
// for that platform right now.
#[rstest]
#[cfg_attr(linux, ignore)]
fn should_support_watching_a_directory_recursively(mut action_std_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap();
let dir = temp.child("dir");
dir.create_dir_all().unwrap();
let file = dir.child("file");
file.touch().unwrap();
// distant action watch {path}
let mut child = action_std_cmd
.args(&["watch", "--recursive", temp.to_str().unwrap()])
.spawn()
.expect("Failed to execute");
// Wait for the process to be ready
wait_a_bit();
// Manipulate the file
file.write_str("some text").unwrap();
// Pause a bit to ensure that the change is detected and reported
wait_even_longer();
let mut stdout = ThreadedReader::new(child.stdout.take().unwrap());
let mut stdout_data = String::new();
while let Some(line) = stdout.try_read_line_timeout(ThreadedReader::default_timeout()) {
stdout_data.push_str(&line);
}
// Close out the process and collect the output
let _ = child.kill().expect("Failed to terminate process");
let output = child.wait_with_output().expect("Failed to wait for output");
let stderr_data = String::from_utf8_lossy(&output.stderr).to_string();
let path = file
.to_path_buf()
.canonicalize()
.unwrap()
.to_str()
.unwrap()
.to_string();
// Verify we get information printed out about the change
assert!(
stdout_data.contains(&path),
"\"{}\" missing {}",
stdout_data,
path
);
assert_eq!(stderr_data, "");
}
#[rstest]
fn yield_an_error_when_fails(mut action_std_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap();
let invalid_path = temp.to_path_buf().join("missing");
// distant action watch {path}
let child = action_std_cmd
.args(&["watch", invalid_path.to_str().unwrap()])
.spawn()
.expect("Failed to execute");
// Pause a bit to ensure that the process started and failed
wait_a_bit();
let output = child
.wait_with_output()
.expect("Failed to wait for child to complete");
// Verify we get information printed out about the change
assert!(!output.status.success(), "Child unexpectedly succeeded");
assert!(output.stdout.is_empty(), "Unexpectedly got stdout");
assert!(!output.stderr.is_empty(), "Missing stderr output");
}
#[rstest]
fn should_support_json_watching_single_file(mut action_std_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("file");
file.touch().unwrap();
// distant action --format json --interactive
let mut cmd = action_std_cmd
.args(&["--format", "json"])
.arg("--interactive")
.spawn()
.expect("Failed to execute");
let mut stdin = cmd.stdin.take().unwrap();
let mut stdout = ThreadedReader::new(cmd.stdout.take().unwrap());
let _ = send_watch_request(&mut stdin, &mut stdout, file.to_path_buf(), false);
// Make a change to some file
file.write_str("some text").unwrap();
// Pause a bit to ensure that the process detected the change and reported it
wait_even_longer();
// Get the response and verify the change
// NOTE: Don't bother checking the kind as it can vary by platform
let res = stdout.read_response_default_timeout();
match &res.payload[0] {
ResponseData::Changed(change) => {
assert_eq!(&change.paths, &[file.to_path_buf().canonicalize().unwrap()]);
}
x => panic!("Unexpected response: {:?}", x),
}
}
#[rstest]
fn should_support_json_watching_directory_recursively(mut action_std_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap();
let dir = temp.child("dir");
dir.create_dir_all().unwrap();
let file = dir.child("file");
file.touch().unwrap();
// distant action --format json --interactive
let mut cmd = action_std_cmd
.args(&["--format", "json"])
.arg("--interactive")
.spawn()
.expect("Failed to execute");
let mut stdin = cmd.stdin.take().unwrap();
let mut stdout = ThreadedReader::new(cmd.stdout.take().unwrap());
let _ = send_watch_request(&mut stdin, &mut stdout, temp.to_path_buf(), true);
// Make a change to some file
file.write_str("some text").unwrap();
// Pause a bit to ensure that the process detected the change and reported it
wait_even_longer();
// Get the response and verify the change
// NOTE: Don't bother checking the kind as it can vary by platform
let res = stdout.read_response_default_timeout();
match &res.payload[0] {
ResponseData::Changed(change) => {
assert_eq!(&change.paths, &[file.to_path_buf().canonicalize().unwrap()]);
}
x => panic!("Unexpected response: {:?}", x),
}
}
#[rstest]
fn should_support_json_reporting_changes_using_correct_request_id(mut action_std_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap();
let file1 = temp.child("file1");
file1.touch().unwrap();
let file2 = temp.child("file2");
file2.touch().unwrap();
// distant action --format json --interactive
let mut cmd = action_std_cmd
.args(&["--format", "json"])
.arg("--interactive")
.spawn()
.expect("Failed to execute");
let mut stdin = cmd.stdin.take().unwrap();
let mut stdout = ThreadedReader::new(cmd.stdout.take().unwrap());
// Create a request to watch file1
let file1_res = send_watch_request(&mut stdin, &mut stdout, file1.to_path_buf(), true);
// Create a request to watch file2
let file2_res = send_watch_request(&mut stdin, &mut stdout, file2.to_path_buf(), true);
assert_ne!(
file1_res.origin_id, file2_res.origin_id,
"Two separate watch responses have same origin id"
);
// Make a change to file1
file1.write_str("some text").unwrap();
// Pause a bit to ensure that the process detected the change and reported it
wait_even_longer();
// Get the response and verify the change
// NOTE: Don't bother checking the kind as it can vary by platform
let file1_change_res = stdout.read_response_default_timeout();
match &file1_change_res.payload[0] {
ResponseData::Changed(change) => {
assert_eq!(
&change.paths,
&[file1.to_path_buf().canonicalize().unwrap()]
);
}
x => panic!("Unexpected response: {:?}", x),
}
// Process any extra messages (we might get create, content, and more)
loop {
// Sleep a bit to give time to get all changes happening
wait_a_bit();
if stdout.try_read_line().is_none() {
break;
}
}
// Make a change to file2
file2.write_str("some text").unwrap();
// Pause a bit to ensure that the process detected the change and reported it
wait_even_longer();
// Get the response and verify the change
// NOTE: Don't bother checking the kind as it can vary by platform
let file2_change_res = stdout.read_response_default_timeout();
match &file2_change_res.payload[0] {
ResponseData::Changed(change) => {
assert_eq!(
&change.paths,
&[file2.to_path_buf().canonicalize().unwrap()]
);
}
x => panic!("Unexpected response: {:?}", x),
}
// Verify that the response origin ids match and are separate
assert_eq!(
file1_res.origin_id, file1_change_res.origin_id,
"File 1 watch origin and change origin are different"
);
assert_eq!(
file2_res.origin_id, file2_change_res.origin_id,
"File 1 watch origin and change origin are different"
);
assert_ne!(
file1_change_res.origin_id, file2_change_res.origin_id,
"Two separate watch change responses have same origin id"
);
}
#[rstest]
fn should_support_json_output_for_error(mut action_std_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap();
let path = temp.to_path_buf().join("missing");
// distant action --format json --interactive
let mut cmd = action_std_cmd
.args(&["--format", "json"])
.arg("--interactive")
.spawn()
.expect("Failed to execute");
let mut stdin = cmd.stdin.take().unwrap();
let mut stdout = ThreadedReader::new(cmd.stdout.take().unwrap());
let req = Request {
id: rand::random(),
tenant: random_tenant(),
payload: vec![RequestData::Watch {
path,
recursive: false,
only: Vec::new(),
except: Vec::new(),
}],
};
// Send our request to the process
let msg = format!("{}\n", serde_json::to_string(&req).unwrap());
stdin
.write_all(msg.as_bytes())
.expect("Failed to write to process");
// Pause a bit to ensure that the process started and processed our request
wait_even_longer();
// Ensure we got an acknowledgement of watching
let res = stdout.read_response_default_timeout();
match &res.payload[0] {
ResponseData::Error(x) => {
assert_eq!(x.kind, ErrorKind::NotFound);
}
x => panic!("Unexpected response: {:?}", x),
}
}

@ -3,7 +3,12 @@ use assert_cmd::Command;
use distant_core::*; use distant_core::*;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use rstest::*; use rstest::*;
use std::{ffi::OsStr, net::SocketAddr, thread}; use std::{
ffi::OsStr,
net::SocketAddr,
process::{Command as StdCommand, Stdio},
thread,
};
use tokio::{runtime::Runtime, sync::mpsc}; use tokio::{runtime::Runtime, sync::mpsc};
const LOG_PATH: &str = "/tmp/test.distant.server.log"; const LOG_PATH: &str = "/tmp/test.distant.server.log";
@ -67,7 +72,7 @@ impl DistantServerCtx {
/// Produces a new test command that configures some distant command /// Produces a new test command that configures some distant command
/// configured with an environment that can talk to a remote distant server /// configured with an environment that can talk to a remote distant server
pub fn new_cmd(&self, subcommand: impl AsRef<OsStr>) -> Command { pub fn new_assert_cmd(&self, subcommand: impl AsRef<OsStr>) -> Command {
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
cmd.arg(subcommand) cmd.arg(subcommand)
.args(&["--session", "environment"]) .args(&["--session", "environment"])
@ -76,6 +81,24 @@ impl DistantServerCtx {
.env("DISTANT_KEY", self.key.as_str()); .env("DISTANT_KEY", self.key.as_str());
cmd cmd
} }
/// Configures some distant command with an environment that can talk to a
/// remote distant server, spawning it as a child process
pub fn new_std_cmd(&self, subcommand: impl AsRef<OsStr>) -> StdCommand {
let cmd_path = assert_cmd::cargo::cargo_bin(env!("CARGO_PKG_NAME"));
let mut cmd = StdCommand::new(cmd_path);
cmd.arg(subcommand)
.args(&["--session", "environment"])
.env("DISTANT_HOST", self.addr.ip().to_string())
.env("DISTANT_PORT", self.addr.port().to_string())
.env("DISTANT_KEY", self.key.as_str())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd
}
} }
impl Drop for DistantServerCtx { impl Drop for DistantServerCtx {
@ -94,10 +117,15 @@ pub fn ctx() -> &'static DistantServerCtx {
#[fixture] #[fixture]
pub fn action_cmd(ctx: &'_ DistantServerCtx) -> Command { pub fn action_cmd(ctx: &'_ DistantServerCtx) -> Command {
ctx.new_cmd("action") ctx.new_assert_cmd("action")
} }
#[fixture] #[fixture]
pub fn lsp_cmd(ctx: &'_ DistantServerCtx) -> Command { pub fn lsp_cmd(ctx: &'_ DistantServerCtx) -> Command {
ctx.new_cmd("lsp") ctx.new_assert_cmd("lsp")
}
#[fixture]
pub fn action_std_cmd(ctx: &'_ DistantServerCtx) -> StdCommand {
ctx.new_std_cmd("action")
} }

@ -26,7 +26,7 @@ pub fn random_tenant() -> String {
/// Initializes logging (should only call once) /// Initializes logging (should only call once)
pub fn init_logging(path: impl Into<PathBuf>) -> flexi_logger::LoggerHandle { pub fn init_logging(path: impl Into<PathBuf>) -> flexi_logger::LoggerHandle {
use flexi_logger::{FileSpec, LevelFilter, LogSpecification, Logger}; use flexi_logger::{FileSpec, LevelFilter, LogSpecification, Logger};
let modules = &["distant", "distant_core"]; let modules = &["distant", "distant_core", "distant_ssh2"];
// Disable logging for everything but our binary, which is based on verbosity // Disable logging for everything but our binary, which is based on verbosity
let mut builder = LogSpecification::builder(); let mut builder = LogSpecification::builder();

Loading…
Cancel
Save