├── .github ├── dependabot.yml_ └── workflows │ ├── build_and_test.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── release.sh ├── src ├── env_reader │ └── mod.rs ├── lib.rs ├── main.rs └── sleeper │ └── mod.rs ├── test.Dockerfile ├── test.sh └── tests └── integration_test.rs /.github/dependabot.yml_: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: '08:00' 8 | open-pull-requests-limit: 99 9 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - develop 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build: 14 | name: Build and Test 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 60 17 | 18 | env: 19 | CARGO_TERM_COLOR: always 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Install rust toolchain 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: stable 28 | override: true 29 | 30 | - name: rustfmt 31 | run: | 32 | cargo fmt --all --check 33 | 34 | - name: clippy 35 | run: | 36 | cargo clippy --all-features --all-targets -- -D warnings 37 | 38 | - name: Build 39 | run: cargo build --verbose 40 | 41 | - name: Run tests 42 | run: cargo test --verbose 43 | 44 | - name: Install cargo-llvm-cov 45 | uses: taiki-e/install-action@cargo-llvm-cov 46 | 47 | - name: Generate code coverage 48 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 49 | 50 | - name: Upload coverage to Codecov 51 | uses: codecov/codecov-action@v5 52 | with: 53 | token: ${{ secrets.CODECOV_TOKEN }} 54 | files: lcov.info 55 | fail_ci_if_error: true 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | permissions: 12 | contents: write 13 | packages: write 14 | 15 | jobs: 16 | build: 17 | strategy: 18 | matrix: 19 | arch: 20 | - { name: x86_64, target: x86_64-unknown-linux-musl } 21 | - { name: aarch64, target: aarch64-unknown-linux-musl } 22 | - { name: armv7, target: armv7-unknown-linux-musleabihf } 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Setup Rust 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: stable 30 | target: ${{ matrix.arch.target }} 31 | override: true 32 | - name: Build 33 | uses: actions-rs/cargo@v1 34 | with: 35 | command: build 36 | args: --release --target=${{ matrix.arch.target }} 37 | use-cross: true 38 | - name: Copy and prepare default artifact for release 39 | if: matrix.arch.name == 'x86_64' 40 | run: | 41 | mkdir -p target/artifacts 42 | cp "target/${{ matrix.arch.target }}/release/wait" "target/artifacts/wait" 43 | - name: Copy and prepare all artifacts for release 44 | run: | 45 | mkdir -p target/artifacts 46 | cp "target/${{ matrix.arch.target }}/release/wait" "target/artifacts/wait_${{ matrix.arch.name }}" 47 | echo "Artifacts list:" 48 | ls -latr target/artifacts/* 49 | - name: Release 50 | uses: softprops/action-gh-release@v1 51 | with: 52 | files: target/artifacts/* 53 | - name: Upload artifacts 54 | uses: actions/upload-artifact@v2 55 | with: 56 | name: wait 57 | path: target/artifacts/* 58 | docker: 59 | needs: build 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Download artifact 63 | uses: actions/download-artifact@v2 64 | with: 65 | name: wait 66 | - name: Set up QEMU 67 | uses: docker/setup-qemu-action@v2 68 | - name: Set up Docker Buildx 69 | uses: docker/setup-buildx-action@v2 70 | - name: Log in to the Container registry 71 | uses: docker/login-action@v2 72 | with: 73 | registry: ghcr.io 74 | username: ${{ github.actor }} 75 | password: ${{ secrets.GITHUB_TOKEN }} 76 | - name: Create Dockerfile 77 | run: | 78 | cat < Dockerfile 79 | FROM alpine AS builder 80 | ARG TARGETPLATFORM 81 | COPY . / 82 | RUN if [ "\$TARGETPLATFORM" = "linux/amd64" ]; then mv /wait_x86_64 /wait; fi; 83 | RUN if [ "\$TARGETPLATFORM" = "linux/arm64" ]; then mv /wait_aarch64 /wait; fi; 84 | RUN if [ "\$TARGETPLATFORM" = "linux/arm/v7" ]; then mv /wait_armv7 /wait; fi; 85 | RUN chmod +x /wait 86 | FROM scratch 87 | COPY --from=builder /wait /wait 88 | EOF 89 | - name: Docker metadata 90 | id: docker-metadata 91 | uses: docker/metadata-action@v4 92 | with: 93 | images: | 94 | ghcr.io/${{ github.repository }} 95 | tags: | 96 | type=semver,pattern={{version}}${{ github.event_name == 'workflow_dispatch' && format(',value={0}', github.event.inputs.version) || '' }} 97 | type=semver,pattern={{major}}.{{minor}}${{ github.event_name == 'workflow_dispatch' && format(',value={0}', github.event.inputs.version) || '' }} 98 | - name: Build and push Docker image 99 | uses: docker/build-push-action@v4 100 | with: 101 | context: . 102 | platforms: linux/amd64, linux/arm64, linux/arm/v7 103 | push: true 104 | tags: ${{ steps.docker-metadata.outputs.tags }} 105 | labels: ${{ steps.docker-metadata.outputs.labels }} 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.idea 2 | **/out/ 3 | **/target/ 4 | **/*.rs.bk 5 | **/*.iml 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "atomic-counter" 7 | version = "1.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "62f447d68cfa5a9ab0c1c862a703da2a65b5ed1b7ce1153c9eb0169506d56019" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "2.8.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 16 | 17 | [[package]] 18 | name = "byteorder" 19 | version = "1.5.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 22 | 23 | [[package]] 24 | name = "cc" 25 | version = "1.2.15" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" 28 | dependencies = [ 29 | "shlex", 30 | ] 31 | 32 | [[package]] 33 | name = "cfg-if" 34 | version = "1.0.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 37 | 38 | [[package]] 39 | name = "env_filter" 40 | version = "0.1.3" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 43 | dependencies = [ 44 | "log", 45 | ] 46 | 47 | [[package]] 48 | name = "env_logger" 49 | version = "0.11.6" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" 52 | dependencies = [ 53 | "env_filter", 54 | "log", 55 | ] 56 | 57 | [[package]] 58 | name = "errno" 59 | version = "0.2.8" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 62 | dependencies = [ 63 | "errno-dragonfly", 64 | "libc", 65 | "winapi", 66 | ] 67 | 68 | [[package]] 69 | name = "errno-dragonfly" 70 | version = "0.1.2" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 73 | dependencies = [ 74 | "cc", 75 | "libc", 76 | ] 77 | 78 | [[package]] 79 | name = "exec" 80 | version = "0.3.1" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615" 83 | dependencies = [ 84 | "errno", 85 | "libc", 86 | ] 87 | 88 | [[package]] 89 | name = "getrandom" 90 | version = "0.3.1" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 93 | dependencies = [ 94 | "cfg-if", 95 | "libc", 96 | "wasi", 97 | "windows-targets", 98 | ] 99 | 100 | [[package]] 101 | name = "lazy_static" 102 | version = "1.5.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 105 | 106 | [[package]] 107 | name = "libc" 108 | version = "0.2.169" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 111 | 112 | [[package]] 113 | name = "log" 114 | version = "0.4.26" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" 117 | 118 | [[package]] 119 | name = "port_check" 120 | version = "0.2.1" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "2110609fb863cdb367d4e69d6c43c81ba6a8c7d18e80082fe9f3ef16b23afeed" 123 | 124 | [[package]] 125 | name = "ppv-lite86" 126 | version = "0.2.20" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 129 | dependencies = [ 130 | "zerocopy 0.7.35", 131 | ] 132 | 133 | [[package]] 134 | name = "proc-macro2" 135 | version = "1.0.93" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 138 | dependencies = [ 139 | "unicode-ident", 140 | ] 141 | 142 | [[package]] 143 | name = "quote" 144 | version = "1.0.38" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 147 | dependencies = [ 148 | "proc-macro2", 149 | ] 150 | 151 | [[package]] 152 | name = "rand" 153 | version = "0.9.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 156 | dependencies = [ 157 | "rand_chacha", 158 | "rand_core", 159 | "zerocopy 0.8.20", 160 | ] 161 | 162 | [[package]] 163 | name = "rand_chacha" 164 | version = "0.9.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 167 | dependencies = [ 168 | "ppv-lite86", 169 | "rand_core", 170 | ] 171 | 172 | [[package]] 173 | name = "rand_core" 174 | version = "0.9.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3" 177 | dependencies = [ 178 | "getrandom", 179 | "zerocopy 0.8.20", 180 | ] 181 | 182 | [[package]] 183 | name = "shell-words" 184 | version = "1.1.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 187 | 188 | [[package]] 189 | name = "shlex" 190 | version = "1.3.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 193 | 194 | [[package]] 195 | name = "syn" 196 | version = "2.0.98" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 199 | dependencies = [ 200 | "proc-macro2", 201 | "quote", 202 | "unicode-ident", 203 | ] 204 | 205 | [[package]] 206 | name = "unicode-ident" 207 | version = "1.0.17" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 210 | 211 | [[package]] 212 | name = "wait" 213 | version = "2.12.1" 214 | dependencies = [ 215 | "atomic-counter", 216 | "env_logger", 217 | "exec", 218 | "lazy_static", 219 | "log", 220 | "port_check", 221 | "rand", 222 | "shell-words", 223 | ] 224 | 225 | [[package]] 226 | name = "wasi" 227 | version = "0.13.3+wasi-0.2.2" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 230 | dependencies = [ 231 | "wit-bindgen-rt", 232 | ] 233 | 234 | [[package]] 235 | name = "winapi" 236 | version = "0.3.9" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 239 | dependencies = [ 240 | "winapi-i686-pc-windows-gnu", 241 | "winapi-x86_64-pc-windows-gnu", 242 | ] 243 | 244 | [[package]] 245 | name = "winapi-i686-pc-windows-gnu" 246 | version = "0.4.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 249 | 250 | [[package]] 251 | name = "winapi-x86_64-pc-windows-gnu" 252 | version = "0.4.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 255 | 256 | [[package]] 257 | name = "windows-targets" 258 | version = "0.52.6" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 261 | dependencies = [ 262 | "windows_aarch64_gnullvm", 263 | "windows_aarch64_msvc", 264 | "windows_i686_gnu", 265 | "windows_i686_gnullvm", 266 | "windows_i686_msvc", 267 | "windows_x86_64_gnu", 268 | "windows_x86_64_gnullvm", 269 | "windows_x86_64_msvc", 270 | ] 271 | 272 | [[package]] 273 | name = "windows_aarch64_gnullvm" 274 | version = "0.52.6" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 277 | 278 | [[package]] 279 | name = "windows_aarch64_msvc" 280 | version = "0.52.6" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 283 | 284 | [[package]] 285 | name = "windows_i686_gnu" 286 | version = "0.52.6" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 289 | 290 | [[package]] 291 | name = "windows_i686_gnullvm" 292 | version = "0.52.6" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 295 | 296 | [[package]] 297 | name = "windows_i686_msvc" 298 | version = "0.52.6" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 301 | 302 | [[package]] 303 | name = "windows_x86_64_gnu" 304 | version = "0.52.6" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 307 | 308 | [[package]] 309 | name = "windows_x86_64_gnullvm" 310 | version = "0.52.6" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 313 | 314 | [[package]] 315 | name = "windows_x86_64_msvc" 316 | version = "0.52.6" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 319 | 320 | [[package]] 321 | name = "wit-bindgen-rt" 322 | version = "0.33.0" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 325 | dependencies = [ 326 | "bitflags", 327 | ] 328 | 329 | [[package]] 330 | name = "zerocopy" 331 | version = "0.7.35" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 334 | dependencies = [ 335 | "byteorder", 336 | "zerocopy-derive 0.7.35", 337 | ] 338 | 339 | [[package]] 340 | name = "zerocopy" 341 | version = "0.8.20" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c" 344 | dependencies = [ 345 | "zerocopy-derive 0.8.20", 346 | ] 347 | 348 | [[package]] 349 | name = "zerocopy-derive" 350 | version = "0.7.35" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 353 | dependencies = [ 354 | "proc-macro2", 355 | "quote", 356 | "syn", 357 | ] 358 | 359 | [[package]] 360 | name = "zerocopy-derive" 361 | version = "0.8.20" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700" 364 | dependencies = [ 365 | "proc-macro2", 366 | "quote", 367 | "syn", 368 | ] 369 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wait" 3 | version = "2.12.1" 4 | authors = ["ufoscout "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | port_check = "0.2" 9 | log = { version = "0.4", default-features = false } 10 | env_logger = { version = "0.11", default-features = false } 11 | exec = { version = "0.3.1", default-features = false } 12 | shell-words = { version = "1.1.0", default-features = false } 13 | 14 | [dev-dependencies] 15 | atomic-counter = "1.0" 16 | lazy_static = "1.4" 17 | rand = "0.9" 18 | 19 | [profile.release] 20 | opt-level = 'z' # Optimize for size. 21 | lto = true 22 | codegen-units = 1 23 | panic = 'abort' 24 | strip = true 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-compose-wait 2 | 3 | ![Build Status](https://github.com/ufoscout/docker-compose-wait/actions/workflows/build_and_test.yml/badge.svg) 4 | [![codecov](https://codecov.io/gh/ufoscout/docker-compose-wait/branch/master/graph/badge.svg)](https://codecov.io/gh/ufoscout/docker-compose-wait) 5 | 6 | A small command-line utility to wait for other docker images to be started while using docker-compose (or Kubernetes or docker stack or whatever). 7 | 8 | It permits waiting for: 9 | - a fixed amount of seconds 10 | - until a TCP port is open on a target image 11 | - until a file or directory is present on the local filesystem 12 | 13 | ## Usage 14 | 15 | This utility should be used in the docker build process and launched before your application starts. 16 | 17 | For example, your application "MySuperApp" uses MongoDB, Postgres and MySql (wow!) and you want to be sure that, when it starts, all other systems are available, then simply customize your dockerfile this way: 18 | 19 | ```dockerfile 20 | ## Use whatever base image 21 | FROM alpine 22 | 23 | ## Add the wait script to the image 24 | COPY --from=ghcr.io/ufoscout/docker-compose-wait:latest /wait /wait 25 | 26 | ## Otherwise you can directly download the executable from github releases. E.g.: 27 | # ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.11.0/wait /wait 28 | # RUN chmod +x /wait 29 | 30 | ## Add your application to the docker image 31 | ADD MySuperApp.sh /MySuperApp.sh 32 | 33 | ## Launch the wait tool and then your application 34 | CMD /wait && /MySuperApp.sh 35 | ``` 36 | 37 | Done! the image is ready. 38 | 39 | Now let's modify the docker-compose.yml file: 40 | 41 | ```yml 42 | version: "3" 43 | 44 | services: 45 | mongo: 46 | image: mongo:3.4 47 | hostname: mongo 48 | ports: 49 | - "27017:27017" 50 | 51 | postgres: 52 | image: "postgres:9.4" 53 | hostname: postgres 54 | ports: 55 | - "5432:5432" 56 | 57 | mysql: 58 | image: "mysql:5.7" 59 | hostname: mysql 60 | ports: 61 | - "3306:3306" 62 | 63 | mySuperApp: 64 | image: "mySuperApp:latest" 65 | hostname: mySuperApp 66 | environment: 67 | WAIT_HOSTS: postgres:5432, mysql:3306, mongo:27017 68 | ``` 69 | 70 | When docker-compose is started (or Kubernetes or docker stack or whatever), your application will be started only when all the pairs host:port in the WAIT_HOSTS variable are available. 71 | The WAIT_HOSTS environment variable is not mandatory, if not declared, the script executes without waiting. 72 | 73 | If you want to use the script directly in docker-compose.yml instead of the Dockerfile, please note that the `command:` configuration option is limited to a single command so you should wrap in a `sh` call. For example: 74 | 75 | ```bash 76 | command: sh -c "/wait && /MySuperApp.sh" 77 | ``` 78 | 79 | This is discussed further [here](https://stackoverflow.com/questions/30063907/using-docker-compose-how-to-execute-multiple-commands) and [here](https://github.com/docker/compose/issues/2033). 80 | 81 | ## Usage in images that do not have a shell 82 | 83 | When using [distroless](https://github.com/GoogleContainerTools/distroless) or building images [`FROM scratch`](https://docs.docker.com/develop/develop-images/baseimages/#create-a-simple-parent-image-using-scratch), it is common to not have `sh` available. In this case, it is necessary to [specify the command for wait to run explicitly](#additional-configuration-options). The invoked command will be invoked with any arguments configured for it and will completely replace the `wait` process in your container via a syscall to [`exec`](https://man7.org/linux/man-pages/man3/exec.3.html). Because there is no shell to expand arguments in this case, `wait` must be the `ENTRYPOINT` for the container and has to be specified in [the exec form](https://docs.docker.com/engine/reference/builder/#exec-form-entrypoint-example). Note that because there is no shell to perform expansion, arguments like `*` must be interpreted by the program that receives them. 84 | 85 | ```dockerfile 86 | FROM golang 87 | 88 | COPY myApp /app 89 | WORKDIR /app 90 | RUN go build -o /myApp -ldflags '-s -w -extldflags -static' ./... 91 | 92 | ## ---------------- 93 | 94 | FROM scratch 95 | 96 | COPY --from=ghcr.io/ufoscout/docker-compose-wait:latest /wait /wait 97 | 98 | COPY --from=0 /myApp /myApp 99 | ENV WAIT_COMMAND="/myApp arg1 argN..." 100 | ENTRYPOINT ["/wait"] 101 | ``` 102 | 103 | ## Additional configuration options 104 | 105 | The behaviour of the wait utility can be configured with the following environment variables: 106 | 107 | - _WAIT_LOGGER_LEVEL_ : the output logger level. Valid values are: _debug_, _info_, _error_, _off_. the default is _debug_. 108 | - _WAIT_HOSTS_: comma-separated list of pairs host:port for which you want to wait. 109 | - _WAIT_PATHS_: comma-separated list of paths (i.e. files or directories) on the local filesystem for which you want to wait until they exist. 110 | - _WAIT_COMMAND_: command and arguments to run once waiting completes. The invoked command will completely replace the `wait` process. The default is none. 111 | - _WAIT_TIMEOUT_: max number of seconds to wait for all the hosts/paths to be available before failure. The default is 30 seconds. 112 | - _WAIT_HOST_CONNECT_TIMEOUT_: The timeout of a single TCP connection to a remote host before attempting a new connection. The default is 5 seconds. 113 | - _WAIT_BEFORE_: number of seconds to wait (sleep) before start checking for the hosts/paths availability 114 | - _WAIT_AFTER_: number of seconds to wait (sleep) once all the hosts/paths are available 115 | - _WAIT_SLEEP_INTERVAL_: number of seconds to sleep between retries. The default is 1 second. 116 | 117 | 118 | ## Supported architectures 119 | 120 | From release 2.11.0, the following executables are available for download: 121 | - _wait_: This is the executable intended for Linux x64 systems 122 | - *wait_x86_64*: This is the very same executable than _wait_ 123 | - *wait_aarch64*: This is the executable to be used for aarch64 architectures 124 | - *wait_arm7*: This is the executable to be used for arm7 architectures 125 | 126 | All executables are built with [MUSL](https://www.musl-libc.org/) for maximum portability. 127 | 128 | To use any of these executables, simply replace the executable name in the download link: 129 | https://github.com/ufoscout/docker-compose-wait/releases/download/{{VERSION}}/{{executable_name}} 130 | 131 | 132 | ## Docker images 133 | 134 | Official docker images based on `scratch` can be found here: 135 | https://github.com/users/ufoscout/packages/container/package/docker-compose-wait 136 | 137 | 138 | ## Using on other systems 139 | 140 | The simplest way of getting the _wait_ executable is to download it from 141 | 142 | [https://github.com/ufoscout/docker-compose-wait/releases/download/{{VERSION}}/wait](https://github.com/ufoscout/docker-compose-wait/releases/download/{{VERSION}}/wait) 143 | 144 | or to use one of the pre-built docker images. 145 | 146 | If you need it for an architecture for which a pre-built file is not available, you should clone this repository and build it for your target. 147 | 148 | As it has no external dependencies, an being written in the mighty [rust](https://www.rust-lang.org) 149 | programming language, the build process is just a simple `cargo build --release` 150 | (well... of course you need to install the rust compiler before...) 151 | 152 | For everything involving cross-compilation, you should take a look at [Cross](https://github.com/rust-embedded/cross). 153 | 154 | For example, to build for a **raspberry pi**, everything you have to do is: 155 | 156 | 1. Install the latest stable rust toolchain using rustup 157 | 2. Correctly configure Docker on your machine 158 | 3. Open a terminal and type: 159 | 160 | ```bash 161 | cargo install cross 162 | cross build --target=armv7-unknown-linux-musleabihf --release 163 | ``` 164 | 165 | Use your shiny new executable on your raspberry device! 166 | 167 | ## Notes 168 | 169 | This utility was explicitly written to be used with docker-compose; however, it can be used everywhere since it has no dependencies on docker. 170 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo test 4 | 5 | cargo build --release --target=x86_64-unknown-linux-musl 6 | -------------------------------------------------------------------------------- /src/env_reader/mod.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | pub fn env_var(key: &str, default: String) -> String { 4 | match env::var(key) { 5 | Ok(val) => val, 6 | Err(_e) => default, 7 | } 8 | } 9 | 10 | pub fn env_var_exists(key: &str) -> bool { 11 | env::var(key).is_ok() 12 | } 13 | 14 | #[cfg(test)] 15 | mod test { 16 | 17 | use super::*; 18 | use std::env; 19 | 20 | #[test] 21 | fn should_return_an_env_variable() { 22 | let mut env_key = String::from(""); 23 | let mut env_value = String::from(""); 24 | 25 | for (key, value) in env::vars() { 26 | // println!("Variable found [{}]: [{}]", key, value); 27 | if !value.trim().is_empty() { 28 | env_key = key; 29 | env_value = value; 30 | } 31 | } 32 | 33 | println!("Result Variable [{}]: [{}]", env_key, env_value); 34 | 35 | assert!(env_var_exists(&env_key)); 36 | assert_ne!(env_value, String::from("")); 37 | assert_eq!(env_value, env_var(&env_key, String::from(""))); 38 | } 39 | 40 | #[test] 41 | fn should_return_the_default_value_if_env_variable_not_present() { 42 | let mut random: i64 = rand::random(); 43 | let env_key = random.to_string(); 44 | random += 10; 45 | 46 | assert!(!env_var_exists(&env_key)); 47 | assert_eq!(random.to_string(), env_var(&env_key, random.to_string())); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use env_reader::env_var_exists; 2 | use log::*; 3 | use std::option::Option; 4 | use std::path::Path; 5 | use std::time::Duration; 6 | 7 | pub mod env_reader; 8 | pub mod sleeper; 9 | 10 | pub struct Command { 11 | pub program: String, 12 | pub argv: Vec, 13 | } 14 | 15 | pub struct Config { 16 | pub hosts: String, 17 | pub paths: String, 18 | pub command: Option<(Command, String)>, 19 | pub global_timeout: u64, 20 | pub tcp_connection_timeout: u64, 21 | pub wait_before: u64, 22 | pub wait_after: u64, 23 | pub wait_sleep_interval: u64, 24 | } 25 | 26 | const LINE_SEPARATOR: &str = "--------------------------------------------------------"; 27 | 28 | pub fn wait(sleep: &mut dyn sleeper::Sleeper, config: &Config, on_timeout: &mut dyn FnMut()) { 29 | info!("{}", LINE_SEPARATOR); 30 | info!(" docker-compose-wait {}", env!("CARGO_PKG_VERSION")); 31 | info!("---------------------------"); 32 | debug!("Starting with configuration:"); 33 | debug!(" - Hosts to be waiting for: [{}]", config.hosts); 34 | debug!(" - Paths to be waiting for: [{}]", config.paths); 35 | debug!( 36 | " - Timeout before failure: {} seconds ", 37 | config.global_timeout 38 | ); 39 | debug!( 40 | " - TCP connection timeout before retry: {} seconds ", 41 | config.tcp_connection_timeout 42 | ); 43 | 44 | if let Some((_, command_string)) = &config.command { 45 | debug!(" - Command to run once ready: {}", command_string); 46 | } 47 | 48 | debug!( 49 | " - Sleeping time before checking for hosts/paths availability: {} seconds", 50 | config.wait_before 51 | ); 52 | debug!( 53 | " - Sleeping time once all hosts/paths are available: {} seconds", 54 | config.wait_after 55 | ); 56 | debug!( 57 | " - Sleeping time between retries: {} seconds", 58 | config.wait_sleep_interval 59 | ); 60 | debug!("{}", LINE_SEPARATOR); 61 | 62 | if config.wait_before > 0 { 63 | info!( 64 | "Waiting {} seconds before checking for hosts/paths availability", 65 | config.wait_before 66 | ); 67 | info!("{}", LINE_SEPARATOR); 68 | sleep.sleep(config.wait_before); 69 | } 70 | 71 | sleep.reset(); 72 | 73 | if !config.hosts.trim().is_empty() { 74 | for host in config.hosts.trim().split(',') { 75 | info!("Checking availability of host [{}]", host); 76 | while !port_check::is_port_reachable_with_timeout( 77 | host.trim(), 78 | Duration::from_secs(config.tcp_connection_timeout), 79 | ) { 80 | info!("Host [{}] not yet available...", host); 81 | if sleep.elapsed(config.global_timeout) { 82 | error!( 83 | "Timeout! After {} seconds some hosts are still not reachable", 84 | config.global_timeout 85 | ); 86 | on_timeout(); 87 | return; 88 | } 89 | sleep.sleep(config.wait_sleep_interval); 90 | } 91 | info!("Host [{}] is now available!", host); 92 | info!("{}", LINE_SEPARATOR); 93 | } 94 | } 95 | 96 | if !config.paths.trim().is_empty() { 97 | for path in config.paths.trim().split(',') { 98 | info!("Checking availability of path [{}]", path); 99 | while !Path::new(path.trim()).exists() { 100 | info!("Path {} not yet available...", path); 101 | if sleep.elapsed(config.global_timeout) { 102 | error!( 103 | "Timeout! After [{}] seconds some paths are still not reachable", 104 | config.global_timeout 105 | ); 106 | on_timeout(); 107 | return; 108 | } 109 | sleep.sleep(config.wait_sleep_interval); 110 | } 111 | info!("Path [{}] is now available!", path); 112 | info!("{}", LINE_SEPARATOR); 113 | } 114 | } 115 | 116 | if config.wait_after > 0 { 117 | info!( 118 | "Waiting {} seconds after hosts/paths availability", 119 | config.wait_after 120 | ); 121 | info!("{}", LINE_SEPARATOR); 122 | sleep.sleep(config.wait_after); 123 | } 124 | 125 | info!("docker-compose-wait - Everything's fine, the application can now start!"); 126 | info!("{}", LINE_SEPARATOR); 127 | 128 | if let Some((command, _)) = &config.command { 129 | let err = exec::Command::new(&command.program) 130 | .args(&command.argv) 131 | .exec(); 132 | panic!("{}", err); 133 | } 134 | } 135 | 136 | pub fn parse_command>( 137 | raw_cmd: S, 138 | ) -> Result, shell_words::ParseError> { 139 | let s = raw_cmd.into(); 140 | let command_string = s.trim().to_string(); 141 | if command_string.is_empty() { 142 | return Ok(None); 143 | } 144 | let mut argv = shell_words::split(&command_string)?; 145 | Ok(Some(( 146 | Command { 147 | program: argv.remove(0), 148 | argv, 149 | }, 150 | command_string, 151 | ))) 152 | } 153 | 154 | pub fn config_from_env() -> Config { 155 | Config { 156 | hosts: env_reader::env_var("WAIT_HOSTS", "".to_string()), 157 | paths: env_reader::env_var("WAIT_PATHS", "".to_string()), 158 | command: parse_command(env_reader::env_var("WAIT_COMMAND", "".to_string())) 159 | .expect("failed to parse command value from environment"), 160 | global_timeout: to_int(&legacy_or_new("WAIT_HOSTS_TIMEOUT", "WAIT_TIMEOUT", ""), 30), 161 | tcp_connection_timeout: to_int( 162 | &env_reader::env_var("WAIT_HOST_CONNECT_TIMEOUT", "".to_string()), 163 | 5, 164 | ), 165 | wait_before: to_int(&legacy_or_new("WAIT_BEFORE_HOSTS", "WAIT_BEFORE", ""), 0), 166 | wait_after: to_int(&legacy_or_new("WAIT_AFTER_HOSTS", "WAIT_AFTER", ""), 0), 167 | wait_sleep_interval: to_int( 168 | &env_reader::env_var("WAIT_SLEEP_INTERVAL", "".to_string()), 169 | 1, 170 | ), 171 | } 172 | } 173 | 174 | fn legacy_or_new(legacy_var_name: &str, var_name: &str, default: &str) -> String { 175 | let mut temp_value = default.to_string(); 176 | if env_var_exists(legacy_var_name) { 177 | warn!( 178 | "Environment variable [{}] is deprecated. Use [{}] instead.", 179 | legacy_var_name, var_name 180 | ); 181 | temp_value = env_reader::env_var(legacy_var_name, temp_value); 182 | } 183 | temp_value = env_reader::env_var(var_name, temp_value); 184 | temp_value 185 | } 186 | 187 | fn to_int(number: &str, default: u64) -> u64 { 188 | match number.parse::() { 189 | Ok(value) => value, 190 | Err(_e) => default, 191 | } 192 | } 193 | 194 | #[cfg(test)] 195 | mod test { 196 | use super::*; 197 | use lazy_static::*; 198 | use std::env; 199 | use std::sync::Mutex; 200 | 201 | lazy_static! { 202 | static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); 203 | } 204 | 205 | #[test] 206 | fn should_return_int_value() { 207 | let value = to_int("32", 0); 208 | assert_eq!(32, value) 209 | } 210 | 211 | #[test] 212 | fn should_return_zero_when_negative_value() { 213 | let value = to_int("-32", 10); 214 | assert_eq!(10, value) 215 | } 216 | 217 | #[test] 218 | fn should_return_zero_when_invalid_value() { 219 | let value = to_int("hello", 0); 220 | assert_eq!(0, value) 221 | } 222 | 223 | #[test] 224 | fn should_return_zero_when_empty_value() { 225 | let value = to_int("", 11); 226 | assert_eq!(11, value) 227 | } 228 | 229 | #[test] 230 | fn config_should_use_default_values() { 231 | let _guard = TEST_MUTEX.lock().unwrap(); 232 | set_env("", "", "10o", "10", "", "abc", ""); 233 | let config = config_from_env(); 234 | assert_eq!("".to_string(), config.hosts); 235 | assert_eq!(30, config.global_timeout); 236 | assert_eq!(5, config.tcp_connection_timeout); 237 | assert_eq!(0, config.wait_before); 238 | assert_eq!(10, config.wait_after); 239 | } 240 | 241 | #[test] 242 | fn should_get_config_values_from_env() { 243 | let _guard = TEST_MUTEX.lock().unwrap(); 244 | set_env("localhost:1234", "20", "2", "3", "4", "23", ""); 245 | let config = config_from_env(); 246 | assert_eq!("localhost:1234".to_string(), config.hosts); 247 | assert_eq!(20, config.global_timeout); 248 | assert_eq!(23, config.tcp_connection_timeout); 249 | assert_eq!(2, config.wait_before); 250 | assert_eq!(3, config.wait_after); 251 | assert_eq!(4, config.wait_sleep_interval); 252 | } 253 | 254 | #[test] 255 | fn should_get_default_config_values() { 256 | let _guard = TEST_MUTEX.lock().unwrap(); 257 | set_env("localhost:1234", "", "", "", "", "", ""); 258 | let config = config_from_env(); 259 | assert_eq!("localhost:1234".to_string(), config.hosts); 260 | assert_eq!(30, config.global_timeout); 261 | assert_eq!(5, config.tcp_connection_timeout); 262 | assert_eq!(0, config.wait_before); 263 | assert_eq!(0, config.wait_after); 264 | assert_eq!(1, config.wait_sleep_interval); 265 | } 266 | 267 | #[test] 268 | #[should_panic] 269 | fn should_panic_when_given_an_invalid_command() { 270 | let _guard = TEST_MUTEX.lock().unwrap(); 271 | set_env("", "", "", "", "", "", "a 'b"); 272 | config_from_env(); 273 | } 274 | 275 | fn set_env( 276 | hosts: &str, 277 | timeout: &str, 278 | before: &str, 279 | after: &str, 280 | sleep: &str, 281 | tcp_timeout: &str, 282 | command: &str, 283 | ) { 284 | // TODO: Audit that the environment access only happens in single-threaded code. 285 | unsafe { env::set_var("WAIT_BEFORE_HOSTS", before) }; 286 | // TODO: Audit that the environment access only happens in single-threaded code. 287 | unsafe { env::set_var("WAIT_AFTER_HOSTS", after) }; 288 | // TODO: Audit that the environment access only happens in single-threaded code. 289 | unsafe { env::set_var("WAIT_HOSTS_TIMEOUT", timeout) }; 290 | // TODO: Audit that the environment access only happens in single-threaded code. 291 | unsafe { env::set_var("WAIT_HOST_CONNECT_TIMEOUT", tcp_timeout) }; 292 | // TODO: Audit that the environment access only happens in single-threaded code. 293 | unsafe { env::set_var("WAIT_HOSTS", hosts) }; 294 | // TODO: Audit that the environment access only happens in single-threaded code. 295 | unsafe { env::set_var("WAIT_SLEEP_INTERVAL", sleep) }; 296 | // TODO: Audit that the environment access only happens in single-threaded code. 297 | unsafe { env::set_var("WAIT_COMMAND", command) }; 298 | } 299 | 300 | #[test] 301 | fn parse_command_fails_when_command_is_invalid() { 302 | assert!(parse_command(" intentionally 'invalid").is_err()) 303 | } 304 | 305 | #[test] 306 | fn parse_command_returns_none_when_command_is_empty() { 307 | for c in &["", " \t\n\r\n"] { 308 | let p = parse_command(c.to_string()).unwrap(); 309 | assert!(p.is_none()); 310 | } 311 | } 312 | 313 | #[test] 314 | fn parse_command_handles_commands_without_args() { 315 | let (command, command_string) = parse_command("ls".to_string()).unwrap().unwrap(); 316 | assert_eq!("ls", command_string); 317 | assert_eq!("ls", command.program); 318 | assert_eq!(Vec::::new(), command.argv); 319 | } 320 | 321 | #[test] 322 | fn parse_command_handles_commands_with_args() { 323 | let (command, command_string) = parse_command("ls -al".to_string()).unwrap().unwrap(); 324 | assert_eq!("ls -al", command_string); 325 | assert_eq!("ls", command.program); 326 | assert_eq!(vec!["-al"], command.argv); 327 | } 328 | 329 | #[test] 330 | fn parse_command_discards_leading_and_trailing_whitespace() { 331 | let (command, command_string) = parse_command(" hello world ".to_string()) 332 | .unwrap() 333 | .unwrap(); 334 | assert_eq!("hello world", command_string); 335 | assert_eq!("hello", command.program); 336 | assert_eq!(vec!["world"], command.argv); 337 | } 338 | 339 | #[test] 340 | fn parse_command_strips_shell_quotes() { 341 | let (command, command_string) = 342 | parse_command(" find . -type \"f\" -name '*.rs' ".to_string()) 343 | .unwrap() 344 | .unwrap(); 345 | assert_eq!("find . -type \"f\" -name '*.rs'", command_string); 346 | assert_eq!("find", command.program); 347 | assert_eq!(vec![".", "-type", "f", "-name", "*.rs"], command.argv); 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | env_logger::init_from_env(env_logger::Env::default().filter_or("WAIT_LOGGER_LEVEL", "debug")); 3 | 4 | let mut sleep = wait::sleeper::new(); 5 | wait::wait(&mut sleep, &wait::config_from_env(), &mut on_timeout); 6 | } 7 | 8 | fn on_timeout() { 9 | std::process::exit(1); 10 | } 11 | -------------------------------------------------------------------------------- /src/sleeper/mod.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::{Duration, Instant}; 3 | 4 | pub trait Sleeper { 5 | fn sleep(&self, duration: u64); 6 | fn reset(&mut self); 7 | fn elapsed(&self, units: u64) -> bool; 8 | } 9 | 10 | struct SecondsSleeper { 11 | started_at: Instant, 12 | } 13 | 14 | impl Default for SecondsSleeper { 15 | fn default() -> Self { 16 | SecondsSleeper { 17 | started_at: Instant::now(), 18 | } 19 | } 20 | } 21 | 22 | impl Sleeper for SecondsSleeper { 23 | fn sleep(&self, duration: u64) { 24 | thread::sleep(Duration::from_secs(duration)) 25 | } 26 | 27 | fn reset(&mut self) { 28 | self.started_at = Instant::now() 29 | } 30 | 31 | fn elapsed(&self, units: u64) -> bool { 32 | self.started_at.elapsed().as_secs() >= units 33 | } 34 | } 35 | 36 | pub struct MillisSleeper { 37 | started_at: Instant, 38 | } 39 | 40 | impl Default for MillisSleeper { 41 | fn default() -> Self { 42 | MillisSleeper { 43 | started_at: Instant::now(), 44 | } 45 | } 46 | } 47 | 48 | impl Sleeper for MillisSleeper { 49 | fn sleep(&self, duration: u64) { 50 | thread::sleep(Duration::from_millis(duration)) 51 | } 52 | 53 | fn reset(&mut self) { 54 | self.started_at = Instant::now() 55 | } 56 | 57 | fn elapsed(&self, units: u64) -> bool { 58 | self.started_at.elapsed().as_millis() >= u128::from(units) 59 | } 60 | } 61 | 62 | struct NoOpsSleeper {} 63 | 64 | impl Sleeper for NoOpsSleeper { 65 | fn sleep(&self, _duration: u64) {} 66 | 67 | fn reset(&mut self) {} 68 | 69 | fn elapsed(&self, _units: u64) -> bool { 70 | true 71 | } 72 | } 73 | 74 | pub fn new() -> impl Sleeper { 75 | SecondsSleeper::default() 76 | } 77 | 78 | pub fn new_no_ops() -> impl Sleeper { 79 | NoOpsSleeper {} 80 | } 81 | 82 | #[cfg(test)] 83 | mod test { 84 | 85 | use super::*; 86 | use std::time::Instant; 87 | 88 | #[test] 89 | fn should_wait_for_a_second() { 90 | let mut sleeper = new(); 91 | assert!(!sleeper.elapsed(1)); 92 | 93 | let start = Instant::now(); 94 | sleeper.sleep(1); 95 | let elapsed_sec = start.elapsed().as_secs(); 96 | assert!(elapsed_sec >= 1); 97 | assert!(sleeper.elapsed(1)); 98 | 99 | sleeper.reset(); 100 | assert!(!sleeper.elapsed(1)); 101 | 102 | sleeper.sleep(1); 103 | assert!(sleeper.elapsed(1)); 104 | assert!(!sleeper.elapsed(2)); 105 | 106 | sleeper.sleep(1); 107 | assert!(sleeper.elapsed(2)); 108 | } 109 | 110 | #[test] 111 | fn should_not_wait() { 112 | let sleeper = new_no_ops(); 113 | 114 | let start = Instant::now(); 115 | sleeper.sleep(10); 116 | let elapsed_sec = start.elapsed().as_secs(); 117 | assert!(elapsed_sec <= 1); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 2 | WORKDIR /tmp 3 | RUN apk add build-base 4 | RUN printf '#include ' > /tmp/hello.c \ 5 | && printf 'int main() { printf("Hello World"); return 0; }' > /tmp/hello.c \ 6 | && gcc -w -static -o /hello /tmp/hello.c 7 | 8 | FROM scratch 9 | COPY --from=0 /hello /hello 10 | COPY ./target/x86_64-unknown-linux-musl/release/wait /wait 11 | ENV WAIT_LOGGER_LEVEL=off 12 | ENV WAIT_COMMAND=/hello 13 | ENTRYPOINT ["/wait"] 14 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -t wait:test -f test.Dockerfile . && docker run --rm wait:test 4 | 5 | #export WAIT_HOSTS=localhost:4748 6 | #export WAIT_PATHS=./target/one 7 | export WAIT_TIMEOUT=10 8 | export WAIT_BEFORE=1 9 | export WAIT_AFTER=2 10 | 11 | ./target/x86_64-unknown-linux-musl/release/wait && echo 'DOOOOOOONEEEEEE' 12 | 13 | export WAIT_COMMAND="echo 'DOOOOOOONEEEEEE WITH WAIT_COMMAND (i.e. does not requrie a shell)'" 14 | 15 | ./target/x86_64-unknown-linux-musl/release/wait 16 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use atomic_counter::AtomicCounter; 2 | use std::fs::{File, create_dir_all}; 3 | use std::net::{Ipv4Addr, SocketAddrV4, TcpListener}; 4 | use std::time::Instant; 5 | use std::{thread, time}; 6 | use wait::sleeper::*; 7 | 8 | #[test] 9 | fn should_wait_for_5_seconds_before() { 10 | let wait_for: u64 = 5; 11 | let start = Instant::now(); 12 | let mut sleeper = MillisSleeper::default(); 13 | wait::wait( 14 | &mut sleeper, 15 | &new_config("", "", 1, wait_for, 0, 1, 1), 16 | &mut on_timeout, 17 | ); 18 | assert!(millis_elapsed(start) >= wait_for) 19 | } 20 | 21 | #[test] 22 | fn should_wait_for_10_seconds_after() { 23 | let wait_for = 10; 24 | let start = Instant::now(); 25 | let mut sleeper = MillisSleeper::default(); 26 | wait::wait( 27 | &mut sleeper, 28 | &new_config("", "", 1, 0, wait_for, 1, 1), 29 | &mut on_timeout, 30 | ); 31 | assert!(millis_elapsed(start) >= wait_for) 32 | } 33 | 34 | #[test] 35 | fn should_wait_before_and_after() { 36 | let wait_for = 10; 37 | let start = Instant::now(); 38 | let mut sleeper = MillisSleeper::default(); 39 | wait::wait( 40 | &mut sleeper, 41 | &new_config("", "", 1, wait_for, wait_for, 1, 1), 42 | &mut on_timeout, 43 | ); 44 | assert!(millis_elapsed(start) >= (wait_for + wait_for)) 45 | } 46 | 47 | #[test] 48 | fn should_execute_without_wait() { 49 | let start = Instant::now(); 50 | let mut sleeper = MillisSleeper::default(); 51 | wait::wait( 52 | &mut sleeper, 53 | &new_config("", "", 1, 0, 0, 1, 1), 54 | &mut on_timeout, 55 | ); 56 | assert!(millis_elapsed(start) <= 5) 57 | } 58 | 59 | #[test] 60 | fn should_sleep_the_specified_time_between_host_checks() { 61 | let start = Instant::now(); 62 | let mut sleeper = MillisSleeper::default(); 63 | wait::wait( 64 | &mut sleeper, 65 | &new_config("198.19.255.255:1", "", 2_000, 0, 0, 10, 1), 66 | &mut on_timeout, 67 | ); 68 | let elapsed = millis_elapsed(start); 69 | assert!(elapsed >= 2010); 70 | assert!(elapsed < 3000); 71 | } 72 | 73 | #[test] 74 | fn should_sleep_the_specified_time_between_path_checks() { 75 | let start = Instant::now(); 76 | let mut sleeper = MillisSleeper::default(); 77 | wait::wait( 78 | &mut sleeper, 79 | &new_config( 80 | "", 81 | "./target/dsfasdfreworthkjiewuryiwghfsikahfsjfskjf", 82 | 2_000, 83 | 0, 84 | 0, 85 | 11, 86 | 1, 87 | ), 88 | &mut on_timeout, 89 | ); 90 | let elapsed = millis_elapsed(start); 91 | assert!(elapsed >= 2000); 92 | assert!(elapsed < 3000); 93 | } 94 | 95 | #[test] 96 | fn should_exit_on_host_timeout() { 97 | let timeout = 25; 98 | let wait_before = 30; 99 | let wait_after = 300; 100 | let hosts = format!("localhost:{}", free_port()); 101 | let paths = ""; 102 | let start = Instant::now(); 103 | let mut sleeper = MillisSleeper::default(); 104 | 105 | let count: atomic_counter::RelaxedCounter = atomic_counter::RelaxedCounter::new(0); 106 | let mut fun = || { 107 | count.inc(); 108 | }; 109 | assert_eq!(0, count.get()); 110 | 111 | wait::wait( 112 | &mut sleeper, 113 | &new_config(&hosts, paths, timeout, wait_before, wait_after, 1, 1), 114 | &mut fun, 115 | ); 116 | 117 | // assert that the on_timeout callback was called 118 | assert_eq!(1, count.get()); 119 | 120 | assert!(millis_elapsed(start) >= timeout + wait_before); 121 | assert!(millis_elapsed(start) < timeout + wait_after); 122 | } 123 | 124 | #[test] 125 | fn should_exit_on_path_timeout() { 126 | let timeout = 25; 127 | let wait_before = 30; 128 | let wait_after = 300; 129 | let hosts = ""; 130 | let paths = "./target/fsafasdfasfasfasfasfw54s664"; 131 | let start = Instant::now(); 132 | let mut sleeper = MillisSleeper::default(); 133 | 134 | let count: atomic_counter::RelaxedCounter = atomic_counter::RelaxedCounter::new(0); 135 | let mut fun = || { 136 | count.inc(); 137 | }; 138 | assert_eq!(0, count.get()); 139 | 140 | wait::wait( 141 | &mut sleeper, 142 | &new_config(hosts, paths, timeout, wait_before, wait_after, 1, 1), 143 | &mut fun, 144 | ); 145 | 146 | // assert that the on_timeout callback was called 147 | assert_eq!(1, count.get()); 148 | 149 | assert!(millis_elapsed(start) >= timeout + wait_before); 150 | assert!(millis_elapsed(start) < timeout + wait_after); 151 | } 152 | 153 | #[test] 154 | fn should_identify_the_open_port() { 155 | let timeout = 500; 156 | let wait_before = 30; 157 | let wait_after = 30; 158 | 159 | let tcp_listener = new_tcp_listener(); 160 | let hosts = tcp_listener.local_addr().unwrap().to_string(); 161 | let paths = ""; 162 | 163 | let start = Instant::now(); 164 | let mut sleeper = MillisSleeper::default(); 165 | 166 | let count: atomic_counter::RelaxedCounter = atomic_counter::RelaxedCounter::new(0); 167 | let mut fun = || { 168 | count.inc(); 169 | }; 170 | assert_eq!(0, count.get()); 171 | 172 | listen_async(tcp_listener); 173 | 174 | thread::sleep(time::Duration::from_millis(250)); 175 | wait::wait( 176 | &mut sleeper, 177 | &new_config(&hosts, paths, timeout, wait_before, wait_after, 1, 1), 178 | &mut fun, 179 | ); 180 | 181 | assert_eq!(0, count.get()); 182 | 183 | assert!(millis_elapsed(start) >= wait_before + wait_after); 184 | assert!(millis_elapsed(start) < timeout + wait_before + wait_after); 185 | } 186 | 187 | #[test] 188 | fn should_wait_for_multiple_hosts() { 189 | let timeout = 500; 190 | let wait_before = 30; 191 | let wait_after = 30; 192 | 193 | let tcp_listener1 = new_tcp_listener(); 194 | let tcp_listener2 = new_tcp_listener(); 195 | let hosts = tcp_listener1.local_addr().unwrap().to_string() 196 | + "," 197 | + &tcp_listener2.local_addr().unwrap().to_string(); 198 | 199 | let paths = ""; 200 | 201 | let start = Instant::now(); 202 | let mut sleeper = MillisSleeper::default(); 203 | 204 | let count: atomic_counter::RelaxedCounter = atomic_counter::RelaxedCounter::new(0); 205 | let mut fun = || { 206 | count.inc(); 207 | }; 208 | assert_eq!(0, count.get()); 209 | 210 | listen_async(tcp_listener1); 211 | listen_async(tcp_listener2); 212 | 213 | thread::sleep(time::Duration::from_millis(250)); 214 | wait::wait( 215 | &mut sleeper, 216 | &new_config(&hosts, paths, timeout, wait_before, wait_after, 1, 1), 217 | &mut fun, 218 | ); 219 | 220 | assert_eq!(0, count.get()); 221 | 222 | assert!(millis_elapsed(start) >= wait_before + wait_after); 223 | assert!(millis_elapsed(start) < timeout + wait_before + wait_after); 224 | } 225 | 226 | #[test] 227 | fn should_wait_for_multiple_paths() { 228 | let timeout = 500; 229 | let wait_before = 30; 230 | let wait_after = 30; 231 | 232 | let hosts = ""; 233 | 234 | let path_1 = format!("./target/{}", rand::random::()); 235 | let path_2 = format!("./target/{}", rand::random::()); 236 | let paths = path_1.clone() + "," + path_2.as_str(); 237 | 238 | let start = Instant::now(); 239 | let mut sleeper = MillisSleeper::default(); 240 | 241 | let count: atomic_counter::RelaxedCounter = atomic_counter::RelaxedCounter::new(0); 242 | let mut fun = || { 243 | count.inc(); 244 | }; 245 | assert_eq!(0, count.get()); 246 | 247 | thread::spawn(move || { 248 | thread::sleep(time::Duration::from_millis(100)); 249 | create_dir_all(&path_1).unwrap(); 250 | println!("Directory created: [{}]", &path_1); 251 | thread::sleep(time::Duration::from_millis(10)); 252 | File::create(&path_2).unwrap(); 253 | println!("File created: [{}]", &path_2); 254 | }); 255 | 256 | wait::wait( 257 | &mut sleeper, 258 | &new_config(hosts, &paths, timeout, wait_before, wait_after, 1, 1), 259 | &mut fun, 260 | ); 261 | 262 | assert_eq!(0, count.get()); 263 | 264 | assert!(millis_elapsed(start) >= wait_before + wait_after); 265 | assert!(millis_elapsed(start) < timeout + wait_before + wait_after); 266 | } 267 | 268 | #[test] 269 | fn should_wait_for_multiple_hosts_and_paths() { 270 | let timeout = 500; 271 | let wait_before = 30; 272 | let wait_after = 30; 273 | 274 | let tcp_listener1 = new_tcp_listener(); 275 | let tcp_listener2 = new_tcp_listener(); 276 | let hosts = tcp_listener1.local_addr().unwrap().to_string() 277 | + "," 278 | + &tcp_listener2.local_addr().unwrap().to_string(); 279 | 280 | let path_1 = format!("./target/{}", rand::random::()); 281 | let path_2 = format!("./target/{}", rand::random::()); 282 | let paths = path_1.clone() + "," + path_2.as_str(); 283 | 284 | let start = Instant::now(); 285 | let mut sleeper = MillisSleeper::default(); 286 | 287 | let count: atomic_counter::RelaxedCounter = atomic_counter::RelaxedCounter::new(0); 288 | let mut fun = || { 289 | count.inc(); 290 | }; 291 | assert_eq!(0, count.get()); 292 | 293 | listen_async(tcp_listener1); 294 | listen_async(tcp_listener2); 295 | 296 | thread::sleep(time::Duration::from_millis(250)); 297 | 298 | thread::spawn(move || { 299 | thread::sleep(time::Duration::from_millis(100)); 300 | create_dir_all(&path_1).unwrap(); 301 | println!("Directory created: [{}]", &path_1); 302 | thread::sleep(time::Duration::from_millis(10)); 303 | File::create(&path_2).unwrap(); 304 | println!("File created: [{}]", &path_2); 305 | }); 306 | 307 | wait::wait( 308 | &mut sleeper, 309 | &new_config(&hosts, &paths, timeout, wait_before, wait_after, 1, 1), 310 | &mut fun, 311 | ); 312 | 313 | assert_eq!(0, count.get()); 314 | 315 | assert!(millis_elapsed(start) >= wait_before + wait_after); 316 | assert!(millis_elapsed(start) < timeout + wait_before + wait_after); 317 | } 318 | 319 | #[test] 320 | fn should_fail_if_not_all_hosts_are_available() { 321 | let timeout = 100; 322 | let wait_before = 30; 323 | let wait_after = 30; 324 | 325 | let tcp_listener1 = new_tcp_listener(); 326 | let hosts = 327 | tcp_listener1.local_addr().unwrap().to_string() + ",127.0.0.1:" + &free_port().to_string(); 328 | let paths = ""; 329 | 330 | let start = Instant::now(); 331 | let mut sleeper = MillisSleeper::default(); 332 | 333 | let count: atomic_counter::RelaxedCounter = atomic_counter::RelaxedCounter::new(0); 334 | let mut fun = || { 335 | count.inc(); 336 | }; 337 | assert_eq!(0, count.get()); 338 | 339 | listen_async(tcp_listener1); 340 | 341 | thread::sleep(time::Duration::from_millis(250)); 342 | wait::wait( 343 | &mut sleeper, 344 | &new_config(&hosts, paths, timeout, wait_before, wait_after, 1, 1), 345 | &mut fun, 346 | ); 347 | 348 | assert_eq!(1, count.get()); 349 | 350 | assert!(millis_elapsed(start) >= wait_before + wait_after); 351 | assert!(millis_elapsed(start) >= timeout + wait_before + wait_after); 352 | } 353 | 354 | #[test] 355 | fn should_fail_if_not_all_paths_are_available() { 356 | let timeout = 500; 357 | let wait_before = 30; 358 | let wait_after = 30; 359 | 360 | let hosts = ""; 361 | 362 | let path_1 = format!("./target/{}", rand::random::()); 363 | let path_2 = format!("./target/{}", rand::random::()); 364 | let paths = path_1.clone() + "," + path_2.as_str(); 365 | 366 | let start = Instant::now(); 367 | let mut sleeper = MillisSleeper::default(); 368 | 369 | let count: atomic_counter::RelaxedCounter = atomic_counter::RelaxedCounter::new(0); 370 | let mut fun = || { 371 | count.inc(); 372 | }; 373 | assert_eq!(0, count.get()); 374 | 375 | thread::spawn(move || { 376 | thread::sleep(time::Duration::from_millis(100)); 377 | create_dir_all(&path_1).unwrap(); 378 | println!("Directory created: [{}]", &path_1); 379 | }); 380 | 381 | wait::wait( 382 | &mut sleeper, 383 | &new_config(hosts, &paths, timeout, wait_before, wait_after, 1, 1), 384 | &mut fun, 385 | ); 386 | 387 | assert_eq!(1, count.get()); 388 | 389 | assert!(millis_elapsed(start) >= wait_before + wait_after); 390 | assert!(millis_elapsed(start) < timeout + wait_before + wait_after); 391 | } 392 | 393 | #[test] 394 | fn should_fail_if_hosts_are_available_but_paths_are_not() { 395 | let timeout = 100; 396 | let wait_before = 30; 397 | let wait_after = 30; 398 | 399 | let tcp_listener1 = new_tcp_listener(); 400 | let hosts = tcp_listener1.local_addr().unwrap().to_string(); 401 | let paths = "./target/sfasfsfsgwe56345ybrtwet235vhffh4254"; 402 | 403 | let start = Instant::now(); 404 | let mut sleeper = MillisSleeper::default(); 405 | 406 | let count: atomic_counter::RelaxedCounter = atomic_counter::RelaxedCounter::new(0); 407 | let mut fun = || { 408 | count.inc(); 409 | }; 410 | assert_eq!(0, count.get()); 411 | 412 | listen_async(tcp_listener1); 413 | 414 | thread::sleep(time::Duration::from_millis(250)); 415 | wait::wait( 416 | &mut sleeper, 417 | &new_config(&hosts, paths, timeout, wait_before, wait_after, 1, 1), 418 | &mut fun, 419 | ); 420 | 421 | assert_eq!(1, count.get()); 422 | 423 | assert!(millis_elapsed(start) >= wait_before + wait_after); 424 | assert!(millis_elapsed(start) >= timeout + wait_before + wait_after); 425 | } 426 | 427 | #[test] 428 | fn should_fail_if_paths_are_available_but_hosts_are_not() { 429 | let timeout = 500; 430 | let wait_before = 30; 431 | let wait_after = 30; 432 | 433 | let hosts = "127.0.0.1:".to_owned() + &free_port().to_string(); 434 | let paths = "./target"; 435 | 436 | let start = Instant::now(); 437 | let mut sleeper = MillisSleeper::default(); 438 | 439 | let count: atomic_counter::RelaxedCounter = atomic_counter::RelaxedCounter::new(0); 440 | let mut fun = || { 441 | count.inc(); 442 | }; 443 | assert_eq!(0, count.get()); 444 | 445 | wait::wait( 446 | &mut sleeper, 447 | &new_config(&hosts, paths, timeout, wait_before, wait_after, 1, 1), 448 | &mut fun, 449 | ); 450 | 451 | assert_eq!(1, count.get()); 452 | 453 | assert!(millis_elapsed(start) >= wait_before + wait_after); 454 | assert!(millis_elapsed(start) < timeout + wait_before + wait_after); 455 | } 456 | 457 | fn on_timeout() {} 458 | 459 | fn new_config( 460 | hosts: &str, 461 | paths: &str, 462 | timeout: u64, 463 | before: u64, 464 | after: u64, 465 | sleep: u64, 466 | tcp_connection_timeout: u64, 467 | ) -> wait::Config { 468 | wait::Config { 469 | hosts: hosts.to_string(), 470 | paths: paths.to_string(), 471 | command: None, 472 | global_timeout: timeout, 473 | tcp_connection_timeout, 474 | wait_before: before, 475 | wait_after: after, 476 | wait_sleep_interval: sleep, 477 | } 478 | } 479 | 480 | fn new_tcp_listener() -> TcpListener { 481 | let loopback = Ipv4Addr::new(127, 0, 0, 1); 482 | let socket = SocketAddrV4::new(loopback, 0); 483 | TcpListener::bind(socket).unwrap() 484 | } 485 | 486 | fn listen_async(listener: TcpListener) { 487 | thread::spawn(move || { 488 | loop { 489 | match listener.accept() { 490 | Ok(_) => { 491 | println!("Connection received!"); 492 | } 493 | Err(_) => { 494 | println!("Error in received connection!"); 495 | } 496 | } 497 | } 498 | }); 499 | } 500 | 501 | fn free_port() -> u16 { 502 | new_tcp_listener().local_addr().unwrap().port() 503 | } 504 | 505 | fn millis_elapsed(start: Instant) -> u64 { 506 | let elapsed = start.elapsed().as_millis(); 507 | println!("Millis elapsed {}", elapsed); 508 | elapsed as u64 509 | } 510 | --------------------------------------------------------------------------------