From 268ec948d602f9ffe03ce959cc2c6c3cf8defa99 Mon Sep 17 00:00:00 2001 From: Chip Senkbeil Date: Wed, 30 Mar 2022 19:32:20 -0500 Subject: [PATCH] Add filesystem watching & remove distant-lua (#102) --- .github/workflows/ci-all.yml | 6 - .github/workflows/ci-linux.yml | 3 - .github/workflows/ci-macos.yml | 3 - .github/workflows/ci-windows.yml | 12 - .github/workflows/release.yml | 125 +--- CHANGELOG.md | 5 + Cargo.lock | 245 ++++---- Cargo.toml | 2 +- README.md | 2 - distant-core/Cargo.toml | 4 +- distant-core/src/client/mod.rs | 2 + distant-core/src/client/session/ext.rs | 71 ++- distant-core/src/client/session/mod.rs | 5 + distant-core/src/client/watcher.rs | 544 ++++++++++++++++++ distant-core/src/constants.rs | 3 + distant-core/src/data.rs | 538 ++++++++++++++++- distant-core/src/server/distant/handler.rs | 512 ++++++++++++++++- distant-core/src/server/distant/state.rs | 139 ++++- distant-lua-tests/.cargo/config.toml | 8 - distant-lua-tests/Cargo.toml | 27 - distant-lua-tests/README.md | 34 -- distant-lua-tests/tests/common/fixtures.rs | 74 --- distant-lua-tests/tests/common/lua.rs | 41 -- distant-lua-tests/tests/common/mod.rs | 4 - distant-lua-tests/tests/common/poll.rs | 17 - distant-lua-tests/tests/common/session.rs | 41 -- distant-lua-tests/tests/lib.rs | 2 - .../tests/lua/async/append_file.rs | 79 --- .../tests/lua/async/append_file_text.rs | 79 --- distant-lua-tests/tests/lua/async/copy.rs | 210 ------- .../tests/lua/async/create_dir.rs | 129 ----- distant-lua-tests/tests/lua/async/exists.rs | 73 --- distant-lua-tests/tests/lua/async/metadata.rs | 238 -------- distant-lua-tests/tests/lua/async/mod.rs | 17 - distant-lua-tests/tests/lua/async/read_dir.rs | 357 ------------ .../tests/lua/async/read_file.rs | 79 --- .../tests/lua/async/read_file_text.rs | 74 --- distant-lua-tests/tests/lua/async/remove.rs | 145 ----- distant-lua-tests/tests/lua/async/rename.rs | 126 ---- distant-lua-tests/tests/lua/async/spawn.rs | 443 -------------- .../tests/lua/async/spawn_pty.rs | 519 ----------------- .../tests/lua/async/spawn_wait.rs | 173 ------ .../tests/lua/async/system_info.rs | 33 -- .../tests/lua/async/write_file.rs | 79 --- .../tests/lua/async/write_file_text.rs | 79 --- distant-lua-tests/tests/lua/mod.rs | 2 - .../tests/lua/sync/append_file.rs | 60 -- .../tests/lua/sync/append_file_text.rs | 60 -- distant-lua-tests/tests/lua/sync/copy.rs | 161 ------ .../tests/lua/sync/create_dir.rs | 91 --- distant-lua-tests/tests/lua/sync/exists.rs | 43 -- distant-lua-tests/tests/lua/sync/metadata.rs | 152 ----- distant-lua-tests/tests/lua/sync/mod.rs | 17 - distant-lua-tests/tests/lua/sync/read_dir.rs | 249 -------- distant-lua-tests/tests/lua/sync/read_file.rs | 51 -- .../tests/lua/sync/read_file_text.rs | 45 -- distant-lua-tests/tests/lua/sync/remove.rs | 94 --- distant-lua-tests/tests/lua/sync/rename.rs | 97 ---- distant-lua-tests/tests/lua/sync/spawn.rs | 291 ---------- distant-lua-tests/tests/lua/sync/spawn_pty.rs | 324 ----------- .../tests/lua/sync/spawn_wait.rs | 141 ----- .../tests/lua/sync/system_info.rs | 18 - .../tests/lua/sync/write_file.rs | 60 -- .../tests/lua/sync/write_file_text.rs | 60 -- distant-lua/.cargo/config.toml | 17 - distant-lua/Cargo.toml | 41 -- distant-lua/README.md | 105 ---- distant-lua/src/constants.rs | 8 - distant-lua/src/lib.rs | 58 -- distant-lua/src/log.rs | 114 ---- distant-lua/src/runtime.rs | 38 -- distant-lua/src/session.rs | 330 ----------- distant-lua/src/session/api.rs | 215 ------- distant-lua/src/session/opts.rs | 374 ------------ distant-lua/src/session/proc.rs | 392 ------------- distant-lua/src/utils.rs | 126 ---- distant-ssh2/src/handler.rs | 11 + distant-ssh2/tests/ssh2/session.rs | 47 ++ src/exit.rs | 24 +- src/output.rs | 22 +- src/subcommand/action.rs | 38 +- tests/cli/action/mod.rs | 1 + tests/cli/action/watch.rs | 526 +++++++++++++++++ tests/cli/fixtures.rs | 36 +- tests/cli/utils.rs | 2 +- 85 files changed, 2615 insertions(+), 7327 deletions(-) create mode 100644 distant-core/src/client/watcher.rs delete mode 100644 distant-lua-tests/.cargo/config.toml delete mode 100644 distant-lua-tests/Cargo.toml delete mode 100644 distant-lua-tests/README.md delete mode 100644 distant-lua-tests/tests/common/fixtures.rs delete mode 100644 distant-lua-tests/tests/common/lua.rs delete mode 100644 distant-lua-tests/tests/common/mod.rs delete mode 100644 distant-lua-tests/tests/common/poll.rs delete mode 100644 distant-lua-tests/tests/common/session.rs delete mode 100644 distant-lua-tests/tests/lib.rs delete mode 100644 distant-lua-tests/tests/lua/async/append_file.rs delete mode 100644 distant-lua-tests/tests/lua/async/append_file_text.rs delete mode 100644 distant-lua-tests/tests/lua/async/copy.rs delete mode 100644 distant-lua-tests/tests/lua/async/create_dir.rs delete mode 100644 distant-lua-tests/tests/lua/async/exists.rs delete mode 100644 distant-lua-tests/tests/lua/async/metadata.rs delete mode 100644 distant-lua-tests/tests/lua/async/mod.rs delete mode 100644 distant-lua-tests/tests/lua/async/read_dir.rs delete mode 100644 distant-lua-tests/tests/lua/async/read_file.rs delete mode 100644 distant-lua-tests/tests/lua/async/read_file_text.rs delete mode 100644 distant-lua-tests/tests/lua/async/remove.rs delete mode 100644 distant-lua-tests/tests/lua/async/rename.rs delete mode 100644 distant-lua-tests/tests/lua/async/spawn.rs delete mode 100644 distant-lua-tests/tests/lua/async/spawn_pty.rs delete mode 100644 distant-lua-tests/tests/lua/async/spawn_wait.rs delete mode 100644 distant-lua-tests/tests/lua/async/system_info.rs delete mode 100644 distant-lua-tests/tests/lua/async/write_file.rs delete mode 100644 distant-lua-tests/tests/lua/async/write_file_text.rs delete mode 100644 distant-lua-tests/tests/lua/mod.rs delete mode 100644 distant-lua-tests/tests/lua/sync/append_file.rs delete mode 100644 distant-lua-tests/tests/lua/sync/append_file_text.rs delete mode 100644 distant-lua-tests/tests/lua/sync/copy.rs delete mode 100644 distant-lua-tests/tests/lua/sync/create_dir.rs delete mode 100644 distant-lua-tests/tests/lua/sync/exists.rs delete mode 100644 distant-lua-tests/tests/lua/sync/metadata.rs delete mode 100644 distant-lua-tests/tests/lua/sync/mod.rs delete mode 100644 distant-lua-tests/tests/lua/sync/read_dir.rs delete mode 100644 distant-lua-tests/tests/lua/sync/read_file.rs delete mode 100644 distant-lua-tests/tests/lua/sync/read_file_text.rs delete mode 100644 distant-lua-tests/tests/lua/sync/remove.rs delete mode 100644 distant-lua-tests/tests/lua/sync/rename.rs delete mode 100644 distant-lua-tests/tests/lua/sync/spawn.rs delete mode 100644 distant-lua-tests/tests/lua/sync/spawn_pty.rs delete mode 100644 distant-lua-tests/tests/lua/sync/spawn_wait.rs delete mode 100644 distant-lua-tests/tests/lua/sync/system_info.rs delete mode 100644 distant-lua-tests/tests/lua/sync/write_file.rs delete mode 100644 distant-lua-tests/tests/lua/sync/write_file_text.rs delete mode 100644 distant-lua/.cargo/config.toml delete mode 100644 distant-lua/Cargo.toml delete mode 100644 distant-lua/README.md delete mode 100644 distant-lua/src/constants.rs delete mode 100644 distant-lua/src/lib.rs delete mode 100644 distant-lua/src/log.rs delete mode 100644 distant-lua/src/runtime.rs delete mode 100644 distant-lua/src/session.rs delete mode 100644 distant-lua/src/session/api.rs delete mode 100644 distant-lua/src/session/opts.rs delete mode 100644 distant-lua/src/session/proc.rs delete mode 100644 distant-lua/src/utils.rs create mode 100644 tests/cli/action/watch.rs diff --git a/.github/workflows/ci-all.yml b/.github/workflows/ci-all.yml index 8941cde..659e1ae 100644 --- a/.github/workflows/ci-all.yml +++ b/.github/workflows/ci-all.yml @@ -29,12 +29,6 @@ jobs: run: cargo clippy -p distant-core --all-targets --verbose --all-features - name: distant-ssh2 (all features) run: cargo clippy -p distant-ssh2 --all-targets --verbose --all-features - - name: distant-lua (lua51 & vendored) - run: (cd distant-lua && cargo clippy --all-targets --verbose --no-default-features --features "lua51,vendored") - shell: bash - - name: distant-lua-tests (lua51 & vendored) - run: (cd distant-lua-tests && cargo clippy --tests --verbose --no-default-features --features "lua51,vendored") - shell: bash - name: distant (all features) run: cargo clippy --all-targets --verbose --all-features diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index ab55939..2933308 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -44,6 +44,3 @@ jobs: - name: Run CLI tests (no default features) run: cargo test --verbose --no-default-features shell: bash - - name: Run Lua tests - run: (cd distant-lua && cargo build) && (cd distant-lua-tests && cargo test --verbose) - shell: bash diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 7d13e11..4873e0a 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -41,6 +41,3 @@ jobs: - name: Run CLI tests (no default features) run: cargo test --verbose --no-default-features shell: bash - - name: Run Lua tests - run: (cd distant-lua && cargo build) && (cd distant-lua-tests && cargo test --verbose) - shell: bash diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index fc576d0..d327d6c 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -43,15 +43,3 @@ jobs: - name: Build CLI (no default features) run: cargo build --verbose --no-default-features 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b969789..3796aec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,10 +6,6 @@ on: - v[0-9]+.[0-9]+.[0-9]+ - v[0-9]+.[0-9]+.[0-9]+-** -env: - LUA_VERSION: 5.1.5 - LUA_FEATURE: lua51 - jobs: macos: name: "Build release on MacOS" @@ -22,11 +18,7 @@ jobs: X86_DIR: target/x86_64-apple-darwin/release ARM_DIR: target/aarch64-apple-darwin/release BUILD_BIN: distant - BUILD_LIB: libdistant_lua.dylib 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: - uses: actions/checkout@v2 - name: Install Rust (x86) @@ -42,24 +34,6 @@ jobs: toolchain: stable target: ${{ env.ARM_ARCH }} - 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) run: | cargo build --release --all-features --target ${{ env.X86_ARCH }} @@ -82,9 +56,6 @@ jobs: name: ${{ env.UPLOAD_NAME }} path: | ${{ env.UNIVERSAL_REL_BIN }} - ${{ env.UNIVERSAL_REL_LIB }} - ${{ env.X86_REL_LIB }} - ${{ env.ARM_REL_LIB }} windows: name: "Build release on Windows" @@ -95,9 +66,7 @@ jobs: X86_ARCH: x86_64-pc-windows-msvc X86_DIR: target/x86_64-pc-windows-msvc/release BUILD_BIN: distant.exe - BUILD_LIB: distant_lua.dll X86_REL_BIN: distant-win64.exe - X86_REL_LIB: distant_lua-win64.dll steps: - uses: actions/checkout@v2 - name: Install Rust (MSVC) @@ -107,19 +76,6 @@ jobs: toolchain: stable target: ${{ env.X86_ARCH }} - 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) run: | cargo build --release --all-features --target ${{ env.X86_ARCH }} @@ -132,7 +88,6 @@ jobs: with: name: ${{ env.UPLOAD_NAME }} path: | - ${{ env.X86_REL_LIB }} ${{ env.X86_REL_BIN }} linux_gnu: @@ -144,9 +99,7 @@ jobs: X86_GNU_ARCH: x86_64-unknown-linux-gnu X86_GNU_DIR: target/x86_64-unknown-linux-gnu/release BUILD_BIN: distant - BUILD_LIB: libdistant_lua.so X86_GNU_REL_BIN: distant-linux64-gnu - X86_GNU_REL_LIB: distant_lua-linux64-gnu.so steps: - uses: actions/checkout@v2 - name: Install Rust (GNU) @@ -156,12 +109,6 @@ jobs: toolchain: stable target: ${{ env.X86_GNU_ARCH }} - 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) run: | cargo build --release --all-features --target ${{ env.X86_GNU_ARCH }} @@ -174,7 +121,6 @@ jobs: with: name: ${{ env.UPLOAD_NAME }} path: | - ${{ env.X86_GNU_REL_LIB }} ${{ env.X86_GNU_REL_BIN }} linux_musl: @@ -188,9 +134,7 @@ jobs: X86_MUSL_ARCH: x86_64-unknown-linux-musl X86_MUSL_DIR: target/x86_64-unknown-linux-musl/release BUILD_BIN: distant - BUILD_LIB: libdistant_lua.so X86_MUSL_REL_BIN: distant-linux64-musl - X86_MUSL_REL_LIB: distant_lua-linux64-musl.so steps: - uses: actions/checkout@v2 - name: Install base dependencies @@ -200,15 +144,6 @@ jobs: run: | curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal - 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) run: | source $HOME/.cargo/env @@ -222,7 +157,6 @@ jobs: with: name: ${{ env.UPLOAD_NAME }} path: | - ${{ env.X86_MUSL_REL_LIB }} ${{ env.X86_MUSL_REL_BIN }} publish: @@ -234,50 +168,32 @@ jobs: env: MACOS: 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_BIN: distant-win64.exe - WIN64_LIB: distant_lua-win64.dll LINUX64_GNU: linux64-gnu LINUX64_GNU_BIN: distant-linux64-gnu - LINUX64_GNU_LIB: distant_lua-linux64-gnu.so LINUX64_MUSL: linux64-musl LINUX64_MUSL_BIN: distant-linux64-musl - LINUX64_MUSL_LIB: distant_lua-linux64-musl.so steps: - uses: actions/download-artifact@v2 - name: Generate MacOS SHA256 checksums run: | 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 echo "SHA_MACOS_BIN=$(cat ${{ env.MACOS_UNIVERSAL_BIN }}.sha256sum)" >> $GITHUB_ENV - name: Generate Win64 SHA256 checksums run: | 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 echo "SHA_WIN64_BIN=$(cat ${{ env.WIN64_BIN }}.sha256sum)" >> $GITHUB_ENV - name: Generate Linux64 (gnu) SHA256 checksums run: | 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 echo "SHA_LINUX64_GNU_BIN=$(cat ${{ env.LINUX64_GNU_BIN }}.sha256sum)" >> $GITHUB_ENV - name: Generate Linux64 (musl) SHA256 checksums run: | 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 echo "SHA_LINUX64_MUSL_BIN=$(cat ${{ env.LINUX64_MUSL_BIN }}.sha256sum)" >> $GITHUB_ENV - name: Determine git tag @@ -302,47 +218,14 @@ jobs: target_commitish: ${{ github.sha }} draft: false 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: | ${{ 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_LIB }} ${{ 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_LIB }} **/*.sha256sum body: | - ## Install Lua library - ### 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 - + ## Binaries 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-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 ## 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_WIN64_LUA_LIB }} ${{ env.SHA_WIN64_BIN }} - ${{ env.SHA_LINUX64_GNU_LUA_LIB }} - ${{ env.SHA_LINUX64_MUSL_LUA_LIB }} ${{ env.SHA_LINUX64_GNU_BIN }} ${{ env.SHA_LINUX64_MUSL_BIN }} ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 209b631..5e66f9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 command for distant cli - Support for JSON communication of ssh auth during launch (cli) - 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 - 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 - Github actions no longer use paths-filter so every PR & commit will test 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 ### Added diff --git a/Cargo.lock b/Cargo.lock index cfd8942..dd3fcc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,6 +579,8 @@ dependencies = [ "hex", "indoc", "log", + "normpath", + "notify", "once_cell", "portable-pty", "predicates", @@ -592,40 +594,6 @@ dependencies = [ "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]] name = "distant-ssh2" version = "0.16.0" @@ -663,15 +631,6 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" -[[package]] -name = "erased-serde" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de9ad4541d99dc22b59134e7ff8dc3d6c988c89ecd7324bf10a8362b07a2afa" -dependencies = [ - "serde", -] - [[package]] name = "event-listener" version = "2.5.1" @@ -710,6 +669,18 @@ dependencies = [ "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]] name = "flexi_logger" version = "0.18.1" @@ -767,6 +738,15 @@ dependencies = [ "libc", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.17" @@ -995,6 +975,26 @@ dependencies = [ "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]] name = "instant" version = "0.1.11" @@ -1037,6 +1037,26 @@ dependencies = [ "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]] name = "lazy_static" version = "1.4.0" @@ -1116,24 +1136,6 @@ dependencies = [ "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]] name = "memchr" version = "2.4.1" @@ -1169,49 +1171,26 @@ dependencies = [ ] [[package]] -name = "miow" -version = "0.3.7" +name = "mio" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", "winapi", ] [[package]] -name = "mlua" -version = "0.7.3" -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" +name = "miow" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1713774a29db53a48932596dc943439dd54eb56a9efaace716719cc10fa82d5b" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "itertools", - "once_cell", - "proc-macro-error", - "proc-macro2", - "quote", - "regex", - "syn", + "winapi", ] [[package]] @@ -1230,6 +1209,34 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "ntapi" version = "0.3.6" @@ -1285,12 +1292,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" -[[package]] -name = "oorandom" -version = "11.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" - [[package]] name = "opaque-debug" version = "0.3.0" @@ -1360,12 +1361,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "paste" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" - [[package]] name = "pest" version = "2.1.3" @@ -1753,12 +1748,6 @@ dependencies = [ "syn", ] -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc_version" version = "0.4.0" @@ -1950,17 +1939,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simplelog" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85d04ae642154220ef00ee82c36fb07853c10a4f2a0ca6719f9991211d2eb959" -dependencies = [ - "chrono", - "log", - "termcolor", -] - [[package]] name = "siphasher" version = "0.3.9" @@ -2116,15 +2094,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "termcolor" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.1.17" @@ -2262,7 +2231,7 @@ dependencies = [ "bytes", "libc", "memchr", - "mio", + "mio 0.7.13", "num_cpus", "once_cell", "parking_lot", @@ -2414,6 +2383,12 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "wasm-bindgen" version = "0.2.78" diff --git a/Cargo.toml b/Cargo.toml index 2a73515..1382847 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" license = "MIT OR Apache-2.0" [workspace] -members = ["distant-core", "distant-lua", "distant-lua-tests", "distant-ssh2"] +members = ["distant-core", "distant-ssh2"] [profile.release] opt-level = 'z' diff --git a/README.md b/README.md index a93bc3d..a55587e 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,6 @@ talk to the server. 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. -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 diff --git a/distant-core/Cargo.toml b/distant-core/Cargo.toml index 1114828..291dfc9 100644 --- a/distant-core/Cargo.toml +++ b/distant-core/Cargo.toml @@ -16,10 +16,12 @@ bitflags = "1.3.2" bytes = "1.1.0" chacha20poly1305 = "0.9.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" hex = "0.4.3" log = "0.4.14" +notify = { version = "5.0.0-pre.14", features = ["serde"] } +normpath = "0.3.2" once_cell = "1.8.0" portable-pty = "0.7.0" rand = { version = "0.8.4", features = ["getrandom"] } diff --git a/distant-core/src/client/mod.rs b/distant-core/src/client/mod.rs index 6ea59c1..1c9c7f2 100644 --- a/distant-core/src/client/mod.rs +++ b/distant-core/src/client/mod.rs @@ -2,7 +2,9 @@ mod lsp; mod process; mod session; mod utils; +mod watcher; pub use lsp::*; pub use process::*; pub use session::*; +pub use watcher::*; diff --git a/distant-core/src/client/session/ext.rs b/distant-core/src/client/session/ext.rs index ad3e90c..b62bd67 100644 --- a/distant-core/src/client/session/ext.rs +++ b/distant-core/src/client/session/ext.rs @@ -1,8 +1,11 @@ use crate::{ - client::{RemoteLspProcess, RemoteProcess, RemoteProcessError, SessionChannel}, + client::{ + RemoteLspProcess, RemoteProcess, RemoteProcessError, SessionChannel, UnwatchError, + WatchError, Watcher, + }, data::{ - DirEntry, Error as Failure, Metadata, PtySize, Request, RequestData, ResponseData, - SystemInfo, + ChangeKindSet, DirEntry, Error as Failure, Metadata, PtySize, Request, RequestData, + ResponseData, SystemInfo, }, net::TransportError, }; @@ -118,6 +121,23 @@ pub trait SessionChannelExt { dst: impl Into, ) -> AsyncReturn<'_, ()>; + /// Watches a remote file or directory + fn watch( + &mut self, + tenant: impl Into, + path: impl Into, + recursive: bool, + only: impl Into, + except: impl Into, + ) -> AsyncReturn<'_, Watcher, WatchError>; + + /// Unwatches a remote file or directory + fn unwatch( + &mut self, + tenant: impl Into, + path: impl Into, + ) -> AsyncReturn<'_, (), UnwatchError>; + /// Spawns a process on the remote machine fn spawn( &mut self, @@ -374,6 +394,51 @@ impl SessionChannelExt for SessionChannel { ) } + fn watch( + &mut self, + tenant: impl Into, + path: impl Into, + recursive: bool, + only: impl Into, + except: impl Into, + ) -> 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, + path: impl Into, + ) -> AsyncReturn<'_, (), UnwatchError> { + fn inner_unwatch( + channel: &mut SessionChannel, + tenant: impl Into, + path: impl Into, + ) -> 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( &mut self, tenant: impl Into, diff --git a/distant-core/src/client/session/mod.rs b/distant-core/src/client/session/mod.rs index f925e4f..c215d8b 100644 --- a/distant-core/src/client/session/mod.rs +++ b/distant-core/src/client/session/mod.rs @@ -128,6 +128,11 @@ impl Session { .await .and_then(convert::identity) } + + /// Convert into underlying channel + pub fn into_channel(self) -> SessionChannel { + self.channel + } } #[cfg(unix)] diff --git a/distant-core/src/client/watcher.rs b/distant-core/src/client/watcher.rs new file mode 100644 index 0000000..f87fe50 --- /dev/null +++ b/distant-core/src/client/watcher.rs @@ -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, + 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, + mut channel: SessionChannel, + path: impl Into, + recursive: bool, + only: impl Into, + except: impl Into, + ) -> Result { + 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 = 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 { + 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, 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::().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::().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::().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::().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::().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); + } +} diff --git a/distant-core/src/constants.rs b/distant-core/src/constants.rs index e293db8..aa8acac 100644 --- a/distant-core/src/constants.rs +++ b/distant-core/src/constants.rs @@ -4,6 +4,9 @@ pub const CLIENT_MAILBOX_CAPACITY: usize = 10000; /// Capacity associated stdin, stdout, and stderr pipes receiving data from remote server pub const CLIENT_PIPE_CAPACITY: usize = 10000; +/// 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 /// per individual `read` call /// diff --git a/distant-core/src/data.rs b/distant-core/src/data.rs index cddb882..03f4739 100644 --- a/distant-core/src/data.rs +++ b/distant-core/src/data.rs @@ -1,9 +1,15 @@ 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 serde::{Deserialize, Serialize}; -use std::{io, num::ParseIntError, path::PathBuf, str::FromStr}; -use strum::AsRefStr; +use std::{ + 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 /// @@ -189,6 +195,39 @@ pub enum RequestData { 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, + + /// Filter to report back changes except these specified changes + #[cfg_attr( + feature = "structopt", + structopt(short, long, possible_values = &ChangeKind::VARIANTS) + )] + #[serde(default)] + except: Vec, + }, + + /// 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 Exists { /// The path to the file or directory on the remote machine @@ -336,6 +375,9 @@ pub enum ResponseData { errors: Vec, }, + /// Response to a filesystem change for some watched file, directory, or symlink + Changed(Change), + /// Response to checking if a path exists Exists { value: bool }, @@ -910,12 +952,446 @@ impl From for ResponseData { } } +impl From for ResponseData { + fn from(x: notify::Error) -> Self { + Self::Error(Error::from(x)) + } +} + impl From for ResponseData { fn from(x: tokio::task::JoinError) -> Self { 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, +} + +impl From 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 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::>().join(\",\")" +)] +pub struct ChangeKindSet(HashSet); + +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 { + self.0.into_iter().collect() + } +} + +impl BitOr for ChangeKindSet { + type Output = Self; + + fn bitor(mut self, rhs: ChangeKindSet) -> Self::Output { + self.extend(rhs.0); + self + } +} + +impl BitOr for ChangeKindSet { + type Output = Self; + + fn bitor(mut self, rhs: ChangeKind) -> Self::Output { + self.0.insert(rhs); + self + } +} + +impl BitOr 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 { + 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 for ChangeKindSet { + fn from_iter>(iter: I) -> Self { + let mut change_set = HashSet::new(); + + for i in iter { + change_set.insert(i); + } + + ChangeKindSet(change_set) + } +} + +impl From for ChangeKindSet { + fn from(change_kind: ChangeKind) -> Self { + let mut set = Self::empty(); + set.insert(change_kind); + set + } +} + +impl From> for ChangeKindSet { + fn from(changes: Vec) -> 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 #[derive(Clone, Debug, Display, PartialEq, Eq, Serialize, Deserialize)] #[display(fmt = "{}: {}", kind, description)] @@ -930,6 +1406,21 @@ pub struct 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 for Error { + fn from(x: String) -> Self { + Self { + kind: ErrorKind::Other, + description: x, + } + } +} + impl From for Error { fn from(x: io::Error) -> Self { Self { @@ -945,6 +1436,47 @@ impl From for io::Error { } } +impl From 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::>() + .join(", ") + ), + } + } +} + impl From for Error { fn from(x: walkdir::Error) -> Self { if x.io_error().is_some() { diff --git a/distant-core/src/server/distant/handler.rs b/distant-core/src/server/distant/handler.rs index e8fe34e..936e0e5 100644 --- a/distant-core/src/server/distant/handler.rs +++ b/distant-core/src/server/distant/handler.rs @@ -1,16 +1,17 @@ use crate::{ data::{ - self, DirEntry, FileType, Metadata, PtySize, Request, RequestData, Response, ResponseData, - RunningProcess, SystemInfo, + self, Change, ChangeKind, ChangeKindSet, DirEntry, FileType, Metadata, PtySize, Request, + RequestData, Response, ResponseData, RunningProcess, SystemInfo, }, server::distant::{ process::{Process, PtyProcess, SimpleProcess}, - state::{ProcessState, State}, + state::{ProcessState, State, WatcherPath}, }, }; use derive_more::{Display, Error, From}; use futures::future; use log::*; +use notify::{Config as WatcherConfig, RecursiveMode, Watcher}; use std::{ env, future::Future, @@ -30,15 +31,17 @@ type ReplyRet = Pin + Send + 'static>>; #[derive(Debug, Display, Error, From)] pub enum ServerError { - IoError(io::Error), - WalkDirError(walkdir::Error), + Io(io::Error), + Notify(notify::Error), + WalkDir(walkdir::Error), } impl From for ResponseData { fn from(x: ServerError) -> Self { match x { - ServerError::IoError(x) => Self::from(x), - ServerError::WalkDirError(x) => Self::from(x), + ServerError::Io(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::Copy { src, dst } => copy(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::Metadata { path, @@ -366,6 +376,205 @@ async fn rename(src: PathBuf, dst: PathBuf) -> Result { Ok(Outgoing::from(ResponseData::Ok)) } +async fn watch( + conn_id: usize, + state: HState, + reply: F, + path: PathBuf, + recursive: bool, + only: Vec, + except: Vec, +) -> Result +where + F: FnMut(Vec) -> ReplyRet + Clone + Send + 'static, +{ + let only = only.into_iter().collect::(); + let except = except.into_iter().collect::(); + 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!(" Watcher configured for precise events", conn_id,), + Ok(false) => debug!( + " Watcher not configured for precise events", + conn_id, + ), + Err(x) => error!( + " 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!(" Watcher configured for notice events", conn_id), + Ok(false) => debug!( + " Watcher not configured for notice events", + conn_id, + ), + Err(x) => error!( + " 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!( + " 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!( + " 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!( + " 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!(" 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!( + " 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!(" 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!(" Unable to initialize watcher", conn_id,), + ))), + } +} + +async fn unwatch(conn_id: usize, state: HState, path: PathBuf) -> Result { + 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!( + " Unable to unwatch as watcher not initialized", + conn_id, + ), + ))) +} + async fn exists(path: PathBuf) -> Result { // 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 @@ -587,7 +796,7 @@ async fn proc_kill(conn_id: usize, state: HState, id: usize) -> Result 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, format!( " Unable to send stdin to process", @@ -631,7 +840,7 @@ async fn proc_resize_pty( return Ok(Outgoing::from(ResponseData::Ok)); } - Err(ServerError::IoError(io::Error::new( + Err(ServerError::Io(io::Error::new( io::ErrorKind::BrokenPipe, format!( " Unable to resize pty to {:?}", @@ -1906,6 +2115,289 @@ mod tests { 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 = 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 = 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::>(), + ); + + 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::>(), + ); + } + + #[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] async fn exists_should_send_true_if_path_exists() { let (conn_id, state, tx, mut rx) = setup(1); diff --git a/distant-core/src/server/distant/state.rs b/distant-core/src/server/distant/state.rs index 53e2c9a..a0a45c8 100644 --- a/distant-core/src/server/distant/state.rs +++ b/distant-core/src/server/distant/state.rs @@ -1,6 +1,19 @@ use super::{InputChannel, ProcessKiller, ProcessPty}; +use crate::data::{ChangeKindSet, ResponseData}; 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) -> ReplyRet + Send + 'static>; +pub type ReplyRet = Pin + Send + 'static>>; /// Holds state related to multiple connections managed by a server #[derive(Default)] @@ -10,6 +23,109 @@ pub struct State { /// List of processes that will be killed when a connection drops client_processes: HashMap>, + + /// Watcher used for filesystem events + pub watcher: Option, + + /// Mapping of Path -> (Reply Fn, recursive) for watcher notifications + pub watcher_paths: HashMap, +} + +#[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(&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, + recursive: bool, + only: impl Into, + ) -> io::Result { + 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 @@ -25,6 +141,27 @@ pub struct ProcessState { } 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 pub fn push_process_state(&mut self, conn_id: usize, process_state: ProcessState) { self.client_processes diff --git a/distant-lua-tests/.cargo/config.toml b/distant-lua-tests/.cargo/config.toml deleted file mode 100644 index 6d87dde..0000000 --- a/distant-lua-tests/.cargo/config.toml +++ /dev/null @@ -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"] diff --git a/distant-lua-tests/Cargo.toml b/distant-lua-tests/Cargo.toml deleted file mode 100644 index 508a8c6..0000000 --- a/distant-lua-tests/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "distant-lua-tests" -description = "Tests for distant-lua crate" -version = "0.0.0" -authors = ["Chip Senkbeil "] -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"] } diff --git a/distant-lua-tests/README.md b/distant-lua-tests/README.md deleted file mode 100644 index ccbe56a..0000000 --- a/distant-lua-tests/README.md +++ /dev/null @@ -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 diff --git a/distant-lua-tests/tests/common/fixtures.rs b/distant-lua-tests/tests/common/fixtures.rs deleted file mode 100644 index 92f843a..0000000 --- a/distant-lua-tests/tests/common/fixtures.rs +++ /dev/null @@ -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 = OnceCell::new(); - - CTX.get_or_init(DistantServerCtx::initialize) -} diff --git a/distant-lua-tests/tests/common/lua.rs b/distant-lua-tests/tests/common/lua.rs deleted file mode 100644 index 2b585dc..0000000 --- a/distant-lua-tests/tests/common/lua.rs +++ /dev/null @@ -1,41 +0,0 @@ -use mlua::prelude::*; -use std::{env, path::PathBuf}; - -pub fn make() -> LuaResult { - 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::>() - .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) -} diff --git a/distant-lua-tests/tests/common/mod.rs b/distant-lua-tests/tests/common/mod.rs deleted file mode 100644 index b5d5544..0000000 --- a/distant-lua-tests/tests/common/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod fixtures; -pub mod lua; -pub mod poll; -pub mod session; diff --git a/distant-lua-tests/tests/common/poll.rs b/distant-lua-tests/tests/common/poll.rs deleted file mode 100644 index c29b388..0000000 --- a/distant-lua-tests/tests/common/poll.rs +++ /dev/null @@ -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 { - let sleep = lua.create_function(|_, ()| { - thread::sleep(Duration::from_millis(10)); - Ok(()) - })?; - - lua.load(chunk! { - local cb = ... - $sleep() - cb() - }) - .into_function() -} diff --git a/distant-lua-tests/tests/common/session.rs b/distant-lua-tests/tests/common/session.rs deleted file mode 100644 index fe4840b..0000000 --- a/distant-lua-tests/tests/common/session.rs +++ /dev/null @@ -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> { - 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() -} diff --git a/distant-lua-tests/tests/lib.rs b/distant-lua-tests/tests/lib.rs deleted file mode 100644 index 650717b..0000000 --- a/distant-lua-tests/tests/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod common; -mod lua; diff --git a/distant-lua-tests/tests/lua/async/append_file.rs b/distant-lua-tests/tests/lua/async/append_file.rs deleted file mode 100644 index 943ce00..0000000 --- a/distant-lua-tests/tests/lua/async/append_file.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/async/append_file_text.rs b/distant-lua-tests/tests/lua/async/append_file_text.rs deleted file mode 100644 index fca7cdf..0000000 --- a/distant-lua-tests/tests/lua/async/append_file_text.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/async/copy.rs b/distant-lua-tests/tests/lua/async/copy.rs deleted file mode 100644 index 56fc2d0..0000000 --- a/distant-lua-tests/tests/lua/async/copy.rs +++ /dev/null @@ -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())); -} diff --git a/distant-lua-tests/tests/lua/async/create_dir.rs b/distant-lua-tests/tests/lua/async/create_dir.rs deleted file mode 100644 index d446371..0000000 --- a/distant-lua-tests/tests/lua/async/create_dir.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/async/exists.rs b/distant-lua-tests/tests/lua/async/exists.rs deleted file mode 100644 index 3254de3..0000000 --- a/distant-lua-tests/tests/lua/async/exists.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/async/metadata.rs b/distant-lua-tests/tests/lua/async/metadata.rs deleted file mode 100644 index 735e6a0..0000000 --- a/distant-lua-tests/tests/lua/async/metadata.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/async/mod.rs b/distant-lua-tests/tests/lua/async/mod.rs deleted file mode 100644 index 6c46fd3..0000000 --- a/distant-lua-tests/tests/lua/async/mod.rs +++ /dev/null @@ -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; diff --git a/distant-lua-tests/tests/lua/async/read_dir.rs b/distant-lua-tests/tests/lua/async/read_dir.rs deleted file mode 100644 index afc4a46..0000000 --- a/distant-lua-tests/tests/lua/async/read_dir.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/async/read_file.rs b/distant-lua-tests/tests/lua/async/read_file.rs deleted file mode 100644 index 27029f4..0000000 --- a/distant-lua-tests/tests/lua/async/read_file.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/async/read_file_text.rs b/distant-lua-tests/tests/lua/async/read_file_text.rs deleted file mode 100644 index c5c29e6..0000000 --- a/distant-lua-tests/tests/lua/async/read_file_text.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/async/remove.rs b/distant-lua-tests/tests/lua/async/remove.rs deleted file mode 100644 index 67616a5..0000000 --- a/distant-lua-tests/tests/lua/async/remove.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/async/rename.rs b/distant-lua-tests/tests/lua/async/rename.rs deleted file mode 100644 index 716661b..0000000 --- a/distant-lua-tests/tests/lua/async/rename.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/async/spawn.rs b/distant-lua-tests/tests/lua/async/spawn.rs deleted file mode 100644 index bbd077b..0000000 --- a/distant-lua-tests/tests/lua/async/spawn.rs +++ /dev/null @@ -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 = Lazy::new(|| assert_fs::TempDir::new().unwrap()); -static SCRIPT_RUNNER: Lazy = Lazy::new(|| String::from("bash")); - -static ECHO_ARGS_TO_STDOUT_SH: Lazy = 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 = 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 = 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 = 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 = - 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 = 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()); -} diff --git a/distant-lua-tests/tests/lua/async/spawn_pty.rs b/distant-lua-tests/tests/lua/async/spawn_pty.rs deleted file mode 100644 index 5aaa05b..0000000 --- a/distant-lua-tests/tests/lua/async/spawn_pty.rs +++ /dev/null @@ -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 = Lazy::new(|| assert_fs::TempDir::new().unwrap()); -static SCRIPT_RUNNER: Lazy = Lazy::new(|| String::from("bash")); - -static ECHO_ARGS_TO_STDOUT_SH: Lazy = 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 = 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 = 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 = 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 = - 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 = 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 = 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()); -} diff --git a/distant-lua-tests/tests/lua/async/spawn_wait.rs b/distant-lua-tests/tests/lua/async/spawn_wait.rs deleted file mode 100644 index 617fdc2..0000000 --- a/distant-lua-tests/tests/lua/async/spawn_wait.rs +++ /dev/null @@ -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 = Lazy::new(|| assert_fs::TempDir::new().unwrap()); -static SCRIPT_RUNNER: Lazy = Lazy::new(|| String::from("bash")); - -static ECHO_ARGS_TO_STDOUT_SH: Lazy = 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 = 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 = - 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 = 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()); -} diff --git a/distant-lua-tests/tests/lua/async/system_info.rs b/distant-lua-tests/tests/lua/async/system_info.rs deleted file mode 100644 index e2d0600..0000000 --- a/distant-lua-tests/tests/lua/async/system_info.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/async/write_file.rs b/distant-lua-tests/tests/lua/async/write_file.rs deleted file mode 100644 index 0ab6414..0000000 --- a/distant-lua-tests/tests/lua/async/write_file.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/async/write_file_text.rs b/distant-lua-tests/tests/lua/async/write_file_text.rs deleted file mode 100644 index 00d6b15..0000000 --- a/distant-lua-tests/tests/lua/async/write_file_text.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/mod.rs b/distant-lua-tests/tests/lua/mod.rs deleted file mode 100644 index 7b89c4c..0000000 --- a/distant-lua-tests/tests/lua/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod r#async; -mod sync; diff --git a/distant-lua-tests/tests/lua/sync/append_file.rs b/distant-lua-tests/tests/lua/sync/append_file.rs deleted file mode 100644 index e9bf264..0000000 --- a/distant-lua-tests/tests/lua/sync/append_file.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/sync/append_file_text.rs b/distant-lua-tests/tests/lua/sync/append_file_text.rs deleted file mode 100644 index 85ed640..0000000 --- a/distant-lua-tests/tests/lua/sync/append_file_text.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/sync/copy.rs b/distant-lua-tests/tests/lua/sync/copy.rs deleted file mode 100644 index 3adbdaa..0000000 --- a/distant-lua-tests/tests/lua/sync/copy.rs +++ /dev/null @@ -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())); -} diff --git a/distant-lua-tests/tests/lua/sync/create_dir.rs b/distant-lua-tests/tests/lua/sync/create_dir.rs deleted file mode 100644 index 53dba47..0000000 --- a/distant-lua-tests/tests/lua/sync/create_dir.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/sync/exists.rs b/distant-lua-tests/tests/lua/sync/exists.rs deleted file mode 100644 index f5c3eae..0000000 --- a/distant-lua-tests/tests/lua/sync/exists.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/sync/metadata.rs b/distant-lua-tests/tests/lua/sync/metadata.rs deleted file mode 100644 index 0e2a56b..0000000 --- a/distant-lua-tests/tests/lua/sync/metadata.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/sync/mod.rs b/distant-lua-tests/tests/lua/sync/mod.rs deleted file mode 100644 index 6c46fd3..0000000 --- a/distant-lua-tests/tests/lua/sync/mod.rs +++ /dev/null @@ -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; diff --git a/distant-lua-tests/tests/lua/sync/read_dir.rs b/distant-lua-tests/tests/lua/sync/read_dir.rs deleted file mode 100644 index 20c7ab3..0000000 --- a/distant-lua-tests/tests/lua/sync/read_dir.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/sync/read_file.rs b/distant-lua-tests/tests/lua/sync/read_file.rs deleted file mode 100644 index 1dadf9e..0000000 --- a/distant-lua-tests/tests/lua/sync/read_file.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/sync/read_file_text.rs b/distant-lua-tests/tests/lua/sync/read_file_text.rs deleted file mode 100644 index cf0ad17..0000000 --- a/distant-lua-tests/tests/lua/sync/read_file_text.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/sync/remove.rs b/distant-lua-tests/tests/lua/sync/remove.rs deleted file mode 100644 index 505b2cf..0000000 --- a/distant-lua-tests/tests/lua/sync/remove.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/sync/rename.rs b/distant-lua-tests/tests/lua/sync/rename.rs deleted file mode 100644 index 4b450f5..0000000 --- a/distant-lua-tests/tests/lua/sync/rename.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/sync/spawn.rs b/distant-lua-tests/tests/lua/sync/spawn.rs deleted file mode 100644 index c0f2dc5..0000000 --- a/distant-lua-tests/tests/lua/sync/spawn.rs +++ /dev/null @@ -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 = Lazy::new(|| assert_fs::TempDir::new().unwrap()); -static SCRIPT_RUNNER: Lazy = Lazy::new(|| String::from("bash")); - -static ECHO_ARGS_TO_STDOUT_SH: Lazy = 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 = 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 = 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 = 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 = - 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 = 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()); -} diff --git a/distant-lua-tests/tests/lua/sync/spawn_pty.rs b/distant-lua-tests/tests/lua/sync/spawn_pty.rs deleted file mode 100644 index 2ab2767..0000000 --- a/distant-lua-tests/tests/lua/sync/spawn_pty.rs +++ /dev/null @@ -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 = Lazy::new(|| assert_fs::TempDir::new().unwrap()); -static SCRIPT_RUNNER: Lazy = Lazy::new(|| String::from("bash")); - -static ECHO_ARGS_TO_STDOUT_SH: Lazy = 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 = 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 = 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 = 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 = - 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 = 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 = 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()); -} diff --git a/distant-lua-tests/tests/lua/sync/spawn_wait.rs b/distant-lua-tests/tests/lua/sync/spawn_wait.rs deleted file mode 100644 index 16ea86e..0000000 --- a/distant-lua-tests/tests/lua/sync/spawn_wait.rs +++ /dev/null @@ -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 = Lazy::new(|| assert_fs::TempDir::new().unwrap()); -static SCRIPT_RUNNER: Lazy = Lazy::new(|| String::from("bash")); - -static ECHO_ARGS_TO_STDOUT_SH: Lazy = 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 = 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 = - 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 = 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()); -} diff --git a/distant-lua-tests/tests/lua/sync/system_info.rs b/distant-lua-tests/tests/lua/sync/system_info.rs deleted file mode 100644 index 4374ae4..0000000 --- a/distant-lua-tests/tests/lua/sync/system_info.rs +++ /dev/null @@ -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()); -} diff --git a/distant-lua-tests/tests/lua/sync/write_file.rs b/distant-lua-tests/tests/lua/sync/write_file.rs deleted file mode 100644 index d2bcb6a..0000000 --- a/distant-lua-tests/tests/lua/sync/write_file.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua-tests/tests/lua/sync/write_file_text.rs b/distant-lua-tests/tests/lua/sync/write_file_text.rs deleted file mode 100644 index e0b9457..0000000 --- a/distant-lua-tests/tests/lua/sync/write_file_text.rs +++ /dev/null @@ -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"); -} diff --git a/distant-lua/.cargo/config.toml b/distant-lua/.cargo/config.toml deleted file mode 100644 index 31da77d..0000000 --- a/distant-lua/.cargo/config.toml +++ /dev/null @@ -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", -] diff --git a/distant-lua/Cargo.toml b/distant-lua/Cargo.toml deleted file mode 100644 index 4c9b19f..0000000 --- a/distant-lua/Cargo.toml +++ /dev/null @@ -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 "] -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" diff --git a/distant-lua/README.md b/distant-lua/README.md deleted file mode 100644 index 92e0833..0000000 --- a/distant-lua/README.md +++ /dev/null @@ -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 diff --git a/distant-lua/src/constants.rs b/distant-lua/src/constants.rs deleted file mode 100644 index ca11cc2..0000000 --- a/distant-lua/src/constants.rs +++ /dev/null @@ -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; diff --git a/distant-lua/src/lib.rs b/distant-lua/src/lib.rs deleted file mode 100644 index 64e422d..0000000 --- a/distant-lua/src/lib.rs +++ /dev/null @@ -1,58 +0,0 @@ -use mlua::prelude::*; - -/// to_value!<'a, T: Serialize + ?Sized>(lua: &'a Lua, t: &T) -> Result> -/// -/// 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 { - 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 { - 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) -} diff --git a/distant-lua/src/log.rs b/distant-lua/src/log.rs deleted file mode 100644 index 9481df5..0000000 --- a/distant-lua/src/log.rs +++ /dev/null @@ -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 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, - - /// 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> = 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 { - 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) -} diff --git a/distant-lua/src/runtime.rs b/distant-lua/src/runtime.rs deleted file mode 100644 index b26402c..0000000 --- a/distant-lua/src/runtime.rs +++ /dev/null @@ -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 = 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` -pub fn block_on(future: F) -> LuaResult -where - F: Future>, -{ - get_runtime()?.block_on(future) -} - -/// Spawns a task on the global runtime for a future that returns a `LuaResult` -pub fn spawn(f: F) -> impl Future> -where - F: Future> + 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(), - }) - }) -} diff --git a/distant-lua/src/session.rs b/distant-lua/src/session.rs deleted file mode 100644 index 5b8b631..0000000 --- a/distant-lua/src/session.rs +++ /dev/null @@ -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 { - let tbl = lua.create_table()?; - - // get_all() -> Vec - tbl.set("get_all", lua.create_function(|_, ()| Session::all())?)?; - - // get_by_id(id: usize) -> Option - 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 - 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>) -> LuaResult -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>> = - Lazy::new(|| RwLock::new(HashMap::new())); - -fn has_session(id: usize) -> LuaResult { - Ok(SESSION_MAP - .read() - .map_err(|x| x.to_string().to_lua_err())? - .contains_key(&id)) -} - -fn with_session(id: usize, f: impl FnOnce(&DistantSession) -> T) -> LuaResult { - 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> { - with_session(id, |session| session.details().cloned()) -} - -fn get_session_channel(id: usize) -> LuaResult { - 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> { - 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 { - 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 { - 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| { - 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| 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::>>()?, - )?; - tbl.set( - "errors", - errors - .iter() - .map(|x| x.to_string()) - .collect::>(), - )?; - - 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); - } -} diff --git a/distant-lua/src/session/api.rs b/distant-lua/src/session/api.rs deleted file mode 100644 index 45d76d4..0000000 --- a/distant-lua/src/session/api.rs +++ /dev/null @@ -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 = 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 }, |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, Vec), - { - 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, - { 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, #[serde(default)] pty: Option, #[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, #[serde(default)] pty: Option, #[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, #[serde(default)] pty: Option, #[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 }, - |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 } -); diff --git a/distant-lua/src/session/opts.rs b/distant-lua/src/session/opts.rs deleted file mode 100644 index 74e717c..0000000 --- a/distant-lua/src/session/opts.rs +++ /dev/null @@ -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 { - match lua_value { - LuaValue::Table(tbl) => Ok(Self { - host: tbl.get("host")?, - port: tbl.get("port")?, - key: tbl.get("key")?, - timeout: { - let milliseconds: Option = 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 { - 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 = tbl.get("mode")?; - match mode { - Some(value) => lua.from_value(value)?, - None => Default::default(), - } - }, - handler: Ssh2AuthHandler { - on_authenticate: { - let f: Option = 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::>(value) - .map_err(|x| io::Error::new(io::ErrorKind::Other, x)) - }), - None => on_authenticate, - } - }, - on_banner: { - let f: Option = tbl.get("on_banner")?; - match f { - Some(f) => Box::new(move |banner| { - let _ = f.call::(banner.to_string()); - }), - None => on_banner, - } - }, - on_host_verify: { - let f: Option = tbl.get("on_host_verify")?; - match f { - Some(f) => Box::new(move |host| { - f.call::(host.to_string()) - .map_err(|x| io::Error::new(io::ErrorKind::Other, x)) - }), - None => on_host_verify, - } - }, - on_error: { - let f: Option = tbl.get("on_error")?; - match f { - Some(f) => Box::new(move |err| { - let _ = f.call::(err.to_string()); - }), - None => on_error, - } - }, - }, - ssh: { - let ssh_tbl: Option = tbl.get("ssh")?; - match ssh_tbl { - Some(value) => lua.from_value(value)?, - None => Default::default(), - } - }, - distant: { - let distant_tbl: Option = tbl.get("distant")?; - match distant_tbl { - Some(value) => LaunchDistantOpts::from_lua(value, lua)?, - None => Default::default(), - } - }, - timeout: { - let milliseconds: Option = 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 | $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 { - 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 = 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 = lua.from_value(x)?; - args.join(" ") - } - } - }, - - use_login_shell: tbl - .get::<_, Option>("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 - } -} diff --git a/distant-lua/src/session/proc.rs b/distant-lua/src/session/proc.rs deleted file mode 100644 index 41485bc..0000000 --- a/distant-lua/src/session/proc.rs +++ /dev/null @@ -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>> = - Lazy::new(|| RwLock::new(HashMap::new())); - -/// Contains mapping of id -> remote lsp process for use in maintaining active processes -static LSP_PROC_MAP: Lazy>> = - 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 { - runtime::get_runtime()?.block_on(Self::from_distant_async(proc)) - } - - pub async fn from_distant_async(proc: $type) -> LuaResult { - let id = proc.id(); - $map_name.write().await.insert(id, proc); - Ok(Self::new(id)) - } - - fn is_active(id: usize) -> LuaResult { - 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>> { - 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> { - // 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>> { - 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> { - // 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> { - runtime::block_on(Self::status_async(id)) - } - - async fn status_async(id: usize) -> LuaResult> { - 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)> { - runtime::block_on(Self::wait_async(id)) - } - - async fn wait_async(id: usize) -> LuaResult<(bool, Option)> { - 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 { - runtime::block_on(Self::output_async(id)) - } - - pub(crate) async fn output_async(id: usize) -> LuaResult { - 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 = 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, -} - -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, - pub stdout: Vec, - pub stderr: Vec, -} - -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); diff --git a/distant-lua/src/utils.rs b/distant-lua/src/utils.rs deleted file mode 100644 index f1191da..0000000 --- a/distant-lua/src/utils.rs +++ /dev/null @@ -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 { - let tbl = lua.create_table()?; - - tbl.set( - "nvim_wrap_async", - lua.create_function(|lua, (async_fn, millis): (LuaFunction, Option)| { - 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> { - 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> { - 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 { - 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 { - static RAND: OnceCell> = 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()) -} diff --git a/distant-ssh2/src/handler.rs b/distant-ssh2/src/handler.rs index 282bd22..2d7fc6c 100644 --- a/distant-ssh2/src/handler.rs +++ b/distant-ssh2/src/handler.rs @@ -49,6 +49,15 @@ struct Outgoing { post_hook: Option, } +impl Outgoing { + pub fn unsupported() -> Self { + Self::from(ResponseData::from(io::Error::new( + io::ErrorKind::Other, + "Unsupported", + ))) + } +} + impl From for Outgoing { fn from(data: ResponseData) -> Self { Self { @@ -88,6 +97,8 @@ pub(super) async fn process( RequestData::Remove { path, force } => remove(session, path, force).await, RequestData::Copy { src, dst } => copy(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::Metadata { path, diff --git a/distant-ssh2/tests/ssh2/session.rs b/distant-ssh2/tests/ssh2/session.rs index 9b1f048..a4f2873 100644 --- a/distant-ssh2/tests/ssh2/session.rs +++ b/distant-ssh2/tests/ssh2/session.rs @@ -1894,3 +1894,50 @@ async fn system_info_should_send_system_info_based_on_binary(#[future] session: 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), + } +} diff --git a/src/exit.rs b/src/exit.rs index 78feb99..05f4373 100644 --- a/src/exit.rs +++ b/src/exit.rs @@ -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 #[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 From for Box { fn from(x: T) -> Self { Box::new(x) diff --git a/src/output.rs b/src/output.rs index db5ade4..c6ca24c 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,6 +1,6 @@ use crate::opt::Format; use distant_core::{ - data::{Error, Metadata, SystemInfo}, + data::{ChangeKind, Error, Metadata, SystemInfo}, Response, ResponseData, }; use log::*; @@ -127,6 +127,26 @@ fn format_shell(data: ResponseData) -> ResponseOut { .join("\n") .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::>() + .join("\n") + ) + .into_bytes(), + ), ResponseData::Exists { value: exists } => { if exists { ResponseOut::StdoutLine(b"true".to_vec()) diff --git a/src/subcommand/action.rs b/src/subcommand/action.rs index 72b7f6b..330825a 100644 --- a/src/subcommand/action.rs +++ b/src/subcommand/action.rs @@ -9,7 +9,8 @@ use crate::{ }; use derive_more::{Display, Error, From}; 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}; @@ -23,6 +24,7 @@ pub enum Error { OperationFailed, RemoteProcess(RemoteProcessError), Transport(TransportError), + Watch(WatchError), } impl ExitCodeError for Error { @@ -42,6 +44,7 @@ impl ExitCodeError for Error { Self::OperationFailed => ExitCode::Software, Self::RemoteProcess(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); 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::(), + except.into_iter().collect::(), + ) + .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 ( _, diff --git a/tests/cli/action/mod.rs b/tests/cli/action/mod.rs index 5170a65..da3800f 100644 --- a/tests/cli/action/mod.rs +++ b/tests/cli/action/mod.rs @@ -13,3 +13,4 @@ mod proc_spawn; mod remove; mod rename; mod system_info; +mod watch; diff --git a/tests/cli/action/watch.rs b/tests/cli/action/watch.rs new file mode 100644 index 0000000..71cb2f7 --- /dev/null +++ b/tests/cli/action/watch.rs @@ -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>, + rx: mpsc::Receiver, +} + +impl ThreadedReader { + pub fn new(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 { + 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 { + 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 { + 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( + writer: &mut W, + reader: &mut ThreadedReader, + path: impl Into, + 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), + } +} diff --git a/tests/cli/fixtures.rs b/tests/cli/fixtures.rs index 166eb7f..c4e3e37 100644 --- a/tests/cli/fixtures.rs +++ b/tests/cli/fixtures.rs @@ -3,7 +3,12 @@ use assert_cmd::Command; use distant_core::*; use once_cell::sync::OnceCell; 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}; 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 /// configured with an environment that can talk to a remote distant server - pub fn new_cmd(&self, subcommand: impl AsRef) -> Command { + pub fn new_assert_cmd(&self, subcommand: impl AsRef) -> Command { let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); cmd.arg(subcommand) .args(&["--session", "environment"]) @@ -76,6 +81,24 @@ impl DistantServerCtx { .env("DISTANT_KEY", self.key.as_str()); 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) -> 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 { @@ -94,10 +117,15 @@ pub fn ctx() -> &'static DistantServerCtx { #[fixture] pub fn action_cmd(ctx: &'_ DistantServerCtx) -> Command { - ctx.new_cmd("action") + ctx.new_assert_cmd("action") } #[fixture] 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") } diff --git a/tests/cli/utils.rs b/tests/cli/utils.rs index 70588f8..99f9c92 100644 --- a/tests/cli/utils.rs +++ b/tests/cli/utils.rs @@ -26,7 +26,7 @@ pub fn random_tenant() -> String { /// Initializes logging (should only call once) pub fn init_logging(path: impl Into) -> flexi_logger::LoggerHandle { 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 let mut builder = LogSpecification::builder();