├── .cargo └── config.toml ├── .github └── workflows │ ├── rust.yml │ ├── rust_audit.yml │ └── rust_release.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── config.toml ├── pkg └── aur │ └── PKGBUILD ├── resources ├── hstdb.service ├── init.zsh ├── send_test └── start_test_server ├── src ├── client.rs ├── config.rs ├── entry.rs ├── lib.rs ├── main.rs ├── message.rs ├── opt.rs ├── run │ ├── import.rs │ └── mod.rs ├── server │ ├── builder.rs │ ├── db.rs │ └── mod.rs └── store │ ├── filter.rs │ └── mod.rs └── tests └── client_server_integration.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-Ctarget-cpu=native"] 3 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | format: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: nightly 20 | components: rustfmt 21 | override: true 22 | 23 | - name: Check formatting 24 | run: cargo fmt -- --check 25 | 26 | clippy: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: actions-rs/toolchain@v1 31 | with: 32 | toolchain: nightly 33 | components: clippy 34 | override: true 35 | 36 | - name: Run clippy 37 | run: cargo clippy --verbose -- -D warnings -D clippy::dbg_macro -D clippy::todo 38 | 39 | test-linux: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v2 43 | 44 | - name: Run tests 45 | run: cargo test --verbose 46 | 47 | - name: Run tests with default features disabled 48 | run: cargo test --no-default-features --verbose 49 | 50 | build-linux: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v2 54 | 55 | - name: Build 56 | run: cargo build --verbose 57 | 58 | test-macos: 59 | runs-on: macos-latest 60 | steps: 61 | - uses: actions/checkout@v2 62 | 63 | - name: Run tests 64 | run: cargo test --verbose 65 | 66 | - name: Run tests with default features disabled 67 | run: cargo test --no-default-features --verbose 68 | 69 | build-macos: 70 | runs-on: macos-latest 71 | steps: 72 | - uses: actions/checkout@v2 73 | 74 | - name: Build 75 | run: cargo build --verbose 76 | -------------------------------------------------------------------------------- /.github/workflows/rust_audit.yml: -------------------------------------------------------------------------------- 1 | name: Rust Security audit 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | audit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions-rs/audit-check@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/rust_release.yml: -------------------------------------------------------------------------------- 1 | # Copied from https://github.com/sagebind/naru/blob/8d29e81a0074b97aff703f42ffa8b5f44f543d05/.github/workflows/release-binaries.yml 2 | 3 | name: release-binaries 4 | on: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | linux-x86_64-musl: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: stable 18 | default: true 19 | 20 | - name: Build binary 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: build 24 | # --no-default-features as sqlite3 doesn't compile with musl 25 | args: --release --target x86_64-unknown-linux-musl --no-default-features 26 | use-cross: true 27 | 28 | - name: Optimize and package binary 29 | run: | 30 | cd target/x86_64-unknown-linux-musl/release 31 | strip hstdb 32 | chmod +x hstdb 33 | tar -c hstdb | gzip > hstdb.tar.gz 34 | 35 | - name: Upload binary 36 | uses: actions/upload-release-asset@v1 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | asset_name: hstdb-${{ github.event.release.tag-name }}-linux-x86_64-musl.tar.gz 41 | asset_path: target/x86_64-unknown-linux-musl/release/hstdb.tar.gz 42 | upload_url: ${{ github.event.release.upload_url }} 43 | asset_content_type: application/octet-stream 44 | 45 | linux-x86_64-gnu: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v1 49 | 50 | - uses: actions-rs/toolchain@v1 51 | with: 52 | profile: minimal 53 | toolchain: stable 54 | default: true 55 | 56 | - name: Build binary 57 | uses: actions-rs/cargo@v1 58 | with: 59 | command: build 60 | args: --release 61 | 62 | - name: Optimize and package binary 63 | run: | 64 | cd target/release 65 | strip hstdb 66 | chmod +x hstdb 67 | tar -c hstdb | gzip > hstdb.tar.gz 68 | 69 | - name: Upload binary 70 | uses: actions/upload-release-asset@v1 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | with: 74 | asset_name: hstdb-${{ github.event.release.tag-name }}-linux-x86_64-gnu.tar.gz 75 | asset_path: target/release/hstdb.tar.gz 76 | upload_url: ${{ github.event.release.upload_url }} 77 | asset_content_type: application/octet-stream 78 | 79 | macos-x86_64: 80 | runs-on: macos-latest 81 | steps: 82 | - uses: actions/checkout@v1 83 | 84 | - uses: actions-rs/toolchain@v1 85 | with: 86 | profile: minimal 87 | toolchain: stable 88 | default: true 89 | 90 | - name: Build binary 91 | uses: actions-rs/cargo@v1 92 | with: 93 | command: build 94 | args: --release 95 | use-cross: true 96 | 97 | - name: Optimize and package binary 98 | run: | 99 | cd target/release 100 | strip hstdb 101 | chmod +x hstdb 102 | mkdir dmg 103 | mv hstdb dmg/ 104 | hdiutil create -fs HFS+ -srcfolder dmg -volname hstdb hstdb.dmg 105 | 106 | - name: Upload binary 107 | uses: actions/upload-release-asset@v1 108 | env: 109 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 110 | with: 111 | asset_name: hstdb-${{ github.event.release.tag-name }}-macos-x86_64.dmg 112 | asset_path: target/release/hstdb.dmg 113 | upload_url: ${{ github.event.release.upload_url }} 114 | asset_content_type: application/octet-stream 115 | 116 | # Maybe someday 117 | # windows-x86_64: 118 | # runs-on: windows-latest 119 | # steps: 120 | # - uses: actions/checkout@v1 121 | # 122 | # - uses: actions-rs/toolchain@v1 123 | # with: 124 | # profile: minimal 125 | # toolchain: stable 126 | # default: true 127 | # 128 | # - name: Build binary 129 | # uses: actions-rs/cargo@v1 130 | # with: 131 | # command: build 132 | # args: --release 133 | # use-cross: true 134 | # 135 | # - name: Upload binary 136 | # uses: actions/upload-release-asset@v1 137 | # env: 138 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 139 | # with: 140 | # asset_name: hstdb-${{ github.event.release.tag-name }}-windows-x86_64.exe 141 | # asset_path: target/release/hstdb.exe 142 | # upload_url: ${{ github.event.release.upload_url }} 143 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /.worktrees/ 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | condense_wildcard_suffixes = true 2 | format_strings = true 3 | imports_granularity = "Crate" 4 | imports_indent = "Block" 5 | imports_layout = "Vertical" 6 | normalize_comments = true 7 | reorder_imports = true 8 | use_try_shorthand = true 9 | wrap_comments = true 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.1 [2021-09-02] 4 | * No longer show an error when piping the output of hstdb and the 5 | pipe has been closed. [#19] 6 | 7 | ## 2.0.0 [2021-08-31] 8 | * Add flag `--session`. Allows to filter entries by the given 9 | session. The session of a history entry can be found using 10 | `--show-session`. 11 | * Add flag `--filter-failed`. Enables filtering of failed commands 12 | when listing the history. Will filter out all commands that had a 13 | return code that is not 0. 14 | * Add option `--find-status`. When specified will find all commands 15 | with the given return code. 16 | * Ignore commands starting with ' ' (space). This should make it 17 | easier to not record sensitive commands. This is configurable in a 18 | configuration file with the option `ignore_space`. By default this 19 | is enabled. 20 | * Add configuration option `log_level` to change the default log level 21 | to run under. 22 | 23 | ## 1.0.0 [2021-06-01] 24 | * No big changes just updated the dependencies. 25 | * Automatic binaries created through github actions. 26 | 27 | ## 0.1.0 28 | 29 | ### Changed 30 | 31 | * Command filter will now only match if entry command matches exactly 32 | [[a5c3785](https://github.com/AlexanderThaller/hstdb/commit/b4a89c2f109b68b901e4610ebe2f39834ffe8d6f)] 33 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 73 | dependencies = [ 74 | "anstyle", 75 | "once_cell", 76 | "windows-sys", 77 | ] 78 | 79 | [[package]] 80 | name = "autocfg" 81 | version = "1.4.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 84 | 85 | [[package]] 86 | name = "bincode" 87 | version = "2.0.1" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" 90 | dependencies = [ 91 | "bincode_derive", 92 | "serde", 93 | "unty", 94 | ] 95 | 96 | [[package]] 97 | name = "bincode_derive" 98 | version = "2.0.1" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" 101 | dependencies = [ 102 | "virtue", 103 | ] 104 | 105 | [[package]] 106 | name = "bitflags" 107 | version = "1.3.2" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 110 | 111 | [[package]] 112 | name = "bitflags" 113 | version = "2.9.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 116 | 117 | [[package]] 118 | name = "bumpalo" 119 | version = "3.17.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 122 | 123 | [[package]] 124 | name = "byteorder" 125 | version = "1.5.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 128 | 129 | [[package]] 130 | name = "cc" 131 | version = "1.2.17" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" 134 | dependencies = [ 135 | "shlex", 136 | ] 137 | 138 | [[package]] 139 | name = "cfg-if" 140 | version = "1.0.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 143 | 144 | [[package]] 145 | name = "cfg_aliases" 146 | version = "0.2.1" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 149 | 150 | [[package]] 151 | name = "chrono" 152 | version = "0.4.40" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" 155 | dependencies = [ 156 | "android-tzdata", 157 | "iana-time-zone", 158 | "js-sys", 159 | "num-traits", 160 | "serde", 161 | "wasm-bindgen", 162 | "windows-link", 163 | ] 164 | 165 | [[package]] 166 | name = "clap" 167 | version = "4.5.35" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" 170 | dependencies = [ 171 | "clap_builder", 172 | "clap_derive", 173 | ] 174 | 175 | [[package]] 176 | name = "clap_builder" 177 | version = "4.5.35" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" 180 | dependencies = [ 181 | "anstream", 182 | "anstyle", 183 | "clap_lex", 184 | "strsim", 185 | ] 186 | 187 | [[package]] 188 | name = "clap_complete" 189 | version = "4.5.47" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6" 192 | dependencies = [ 193 | "clap", 194 | ] 195 | 196 | [[package]] 197 | name = "clap_derive" 198 | version = "4.5.32" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 201 | dependencies = [ 202 | "heck", 203 | "proc-macro2", 204 | "quote", 205 | "syn", 206 | ] 207 | 208 | [[package]] 209 | name = "clap_lex" 210 | version = "0.7.4" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 213 | 214 | [[package]] 215 | name = "colorchoice" 216 | version = "1.0.3" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 219 | 220 | [[package]] 221 | name = "comfy-table" 222 | version = "7.1.4" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" 225 | dependencies = [ 226 | "crossterm", 227 | "unicode-segmentation", 228 | "unicode-width", 229 | ] 230 | 231 | [[package]] 232 | name = "core-foundation-sys" 233 | version = "0.8.7" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 236 | 237 | [[package]] 238 | name = "crc32fast" 239 | version = "1.4.2" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 242 | dependencies = [ 243 | "cfg-if", 244 | ] 245 | 246 | [[package]] 247 | name = "crossbeam-epoch" 248 | version = "0.9.18" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 251 | dependencies = [ 252 | "crossbeam-utils", 253 | ] 254 | 255 | [[package]] 256 | name = "crossbeam-utils" 257 | version = "0.8.21" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 260 | 261 | [[package]] 262 | name = "crossterm" 263 | version = "0.28.1" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 266 | dependencies = [ 267 | "bitflags 2.9.0", 268 | "crossterm_winapi", 269 | "parking_lot 0.12.3", 270 | "rustix 0.38.44", 271 | "winapi", 272 | ] 273 | 274 | [[package]] 275 | name = "crossterm_winapi" 276 | version = "0.9.1" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 279 | dependencies = [ 280 | "winapi", 281 | ] 282 | 283 | [[package]] 284 | name = "csv" 285 | version = "1.3.1" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" 288 | dependencies = [ 289 | "csv-core", 290 | "itoa", 291 | "ryu", 292 | "serde", 293 | ] 294 | 295 | [[package]] 296 | name = "csv-core" 297 | version = "0.1.12" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" 300 | dependencies = [ 301 | "memchr", 302 | ] 303 | 304 | [[package]] 305 | name = "ctrlc" 306 | version = "3.4.6" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" 309 | dependencies = [ 310 | "nix", 311 | "windows-sys", 312 | ] 313 | 314 | [[package]] 315 | name = "diff" 316 | version = "0.1.13" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 319 | 320 | [[package]] 321 | name = "directories" 322 | version = "6.0.0" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" 325 | dependencies = [ 326 | "dirs-sys", 327 | ] 328 | 329 | [[package]] 330 | name = "dirs-sys" 331 | version = "0.5.0" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 334 | dependencies = [ 335 | "libc", 336 | "option-ext", 337 | "redox_users", 338 | "windows-sys", 339 | ] 340 | 341 | [[package]] 342 | name = "env_filter" 343 | version = "0.1.3" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 346 | dependencies = [ 347 | "log", 348 | ] 349 | 350 | [[package]] 351 | name = "env_logger" 352 | version = "0.11.8" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 355 | dependencies = [ 356 | "anstream", 357 | "anstyle", 358 | "env_filter", 359 | "log", 360 | ] 361 | 362 | [[package]] 363 | name = "equivalent" 364 | version = "1.0.2" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 367 | 368 | [[package]] 369 | name = "errno" 370 | version = "0.3.11" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 373 | dependencies = [ 374 | "libc", 375 | "windows-sys", 376 | ] 377 | 378 | [[package]] 379 | name = "exitcode" 380 | version = "1.1.2" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" 383 | 384 | [[package]] 385 | name = "fallible-iterator" 386 | version = "0.3.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 389 | 390 | [[package]] 391 | name = "fallible-streaming-iterator" 392 | version = "0.1.9" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 395 | 396 | [[package]] 397 | name = "fastrand" 398 | version = "2.3.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 401 | 402 | [[package]] 403 | name = "flume" 404 | version = "0.11.1" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 407 | dependencies = [ 408 | "futures-core", 409 | "futures-sink", 410 | "nanorand", 411 | "spin", 412 | ] 413 | 414 | [[package]] 415 | name = "foldhash" 416 | version = "0.1.5" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 419 | 420 | [[package]] 421 | name = "fs2" 422 | version = "0.4.3" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" 425 | dependencies = [ 426 | "libc", 427 | "winapi", 428 | ] 429 | 430 | [[package]] 431 | name = "futures-core" 432 | version = "0.3.31" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 435 | 436 | [[package]] 437 | name = "futures-sink" 438 | version = "0.3.31" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 441 | 442 | [[package]] 443 | name = "fxhash" 444 | version = "0.2.1" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 447 | dependencies = [ 448 | "byteorder", 449 | ] 450 | 451 | [[package]] 452 | name = "getrandom" 453 | version = "0.2.15" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 456 | dependencies = [ 457 | "cfg-if", 458 | "js-sys", 459 | "libc", 460 | "wasi 0.11.0+wasi-snapshot-preview1", 461 | "wasm-bindgen", 462 | ] 463 | 464 | [[package]] 465 | name = "getrandom" 466 | version = "0.3.2" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 469 | dependencies = [ 470 | "cfg-if", 471 | "libc", 472 | "r-efi", 473 | "wasi 0.14.2+wasi-0.2.4", 474 | ] 475 | 476 | [[package]] 477 | name = "glob" 478 | version = "0.3.2" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 481 | 482 | [[package]] 483 | name = "hashbrown" 484 | version = "0.15.2" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 487 | dependencies = [ 488 | "foldhash", 489 | ] 490 | 491 | [[package]] 492 | name = "hashlink" 493 | version = "0.10.0" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 496 | dependencies = [ 497 | "hashbrown", 498 | ] 499 | 500 | [[package]] 501 | name = "heck" 502 | version = "0.5.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 505 | 506 | [[package]] 507 | name = "hostname" 508 | version = "0.4.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" 511 | dependencies = [ 512 | "cfg-if", 513 | "libc", 514 | "windows", 515 | ] 516 | 517 | [[package]] 518 | name = "hstdb" 519 | version = "3.0.0" 520 | dependencies = [ 521 | "bincode", 522 | "chrono", 523 | "clap", 524 | "clap_complete", 525 | "comfy-table", 526 | "crossbeam-utils", 527 | "csv", 528 | "ctrlc", 529 | "directories", 530 | "env_logger", 531 | "exitcode", 532 | "flume", 533 | "glob", 534 | "hostname", 535 | "humantime", 536 | "log", 537 | "pretty_assertions", 538 | "regex", 539 | "rusqlite", 540 | "serde", 541 | "sled", 542 | "tempfile", 543 | "thiserror", 544 | "toml", 545 | "uuid", 546 | ] 547 | 548 | [[package]] 549 | name = "humantime" 550 | version = "2.2.0" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" 553 | 554 | [[package]] 555 | name = "iana-time-zone" 556 | version = "0.1.63" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 559 | dependencies = [ 560 | "android_system_properties", 561 | "core-foundation-sys", 562 | "iana-time-zone-haiku", 563 | "js-sys", 564 | "log", 565 | "wasm-bindgen", 566 | "windows-core 0.61.0", 567 | ] 568 | 569 | [[package]] 570 | name = "iana-time-zone-haiku" 571 | version = "0.1.2" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 574 | dependencies = [ 575 | "cc", 576 | ] 577 | 578 | [[package]] 579 | name = "indexmap" 580 | version = "2.8.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" 583 | dependencies = [ 584 | "equivalent", 585 | "hashbrown", 586 | ] 587 | 588 | [[package]] 589 | name = "instant" 590 | version = "0.1.13" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" 593 | dependencies = [ 594 | "cfg-if", 595 | ] 596 | 597 | [[package]] 598 | name = "is_terminal_polyfill" 599 | version = "1.70.1" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 602 | 603 | [[package]] 604 | name = "itoa" 605 | version = "1.0.15" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 608 | 609 | [[package]] 610 | name = "js-sys" 611 | version = "0.3.77" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 614 | dependencies = [ 615 | "once_cell", 616 | "wasm-bindgen", 617 | ] 618 | 619 | [[package]] 620 | name = "libc" 621 | version = "0.2.171" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 624 | 625 | [[package]] 626 | name = "libredox" 627 | version = "0.1.3" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 630 | dependencies = [ 631 | "bitflags 2.9.0", 632 | "libc", 633 | ] 634 | 635 | [[package]] 636 | name = "libsqlite3-sys" 637 | version = "0.32.0" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" 640 | dependencies = [ 641 | "pkg-config", 642 | "vcpkg", 643 | ] 644 | 645 | [[package]] 646 | name = "linux-raw-sys" 647 | version = "0.4.15" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 650 | 651 | [[package]] 652 | name = "linux-raw-sys" 653 | version = "0.9.3" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 656 | 657 | [[package]] 658 | name = "lock_api" 659 | version = "0.4.12" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 662 | dependencies = [ 663 | "autocfg", 664 | "scopeguard", 665 | ] 666 | 667 | [[package]] 668 | name = "log" 669 | version = "0.4.27" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 672 | dependencies = [ 673 | "serde", 674 | ] 675 | 676 | [[package]] 677 | name = "memchr" 678 | version = "2.7.4" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 681 | 682 | [[package]] 683 | name = "nanorand" 684 | version = "0.7.0" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 687 | dependencies = [ 688 | "getrandom 0.2.15", 689 | ] 690 | 691 | [[package]] 692 | name = "nix" 693 | version = "0.29.0" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 696 | dependencies = [ 697 | "bitflags 2.9.0", 698 | "cfg-if", 699 | "cfg_aliases", 700 | "libc", 701 | ] 702 | 703 | [[package]] 704 | name = "num-traits" 705 | version = "0.2.19" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 708 | dependencies = [ 709 | "autocfg", 710 | ] 711 | 712 | [[package]] 713 | name = "once_cell" 714 | version = "1.21.3" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 717 | 718 | [[package]] 719 | name = "option-ext" 720 | version = "0.2.0" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 723 | 724 | [[package]] 725 | name = "parking_lot" 726 | version = "0.11.2" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 729 | dependencies = [ 730 | "instant", 731 | "lock_api", 732 | "parking_lot_core 0.8.6", 733 | ] 734 | 735 | [[package]] 736 | name = "parking_lot" 737 | version = "0.12.3" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 740 | dependencies = [ 741 | "lock_api", 742 | "parking_lot_core 0.9.10", 743 | ] 744 | 745 | [[package]] 746 | name = "parking_lot_core" 747 | version = "0.8.6" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" 750 | dependencies = [ 751 | "cfg-if", 752 | "instant", 753 | "libc", 754 | "redox_syscall 0.2.16", 755 | "smallvec", 756 | "winapi", 757 | ] 758 | 759 | [[package]] 760 | name = "parking_lot_core" 761 | version = "0.9.10" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 764 | dependencies = [ 765 | "cfg-if", 766 | "libc", 767 | "redox_syscall 0.5.10", 768 | "smallvec", 769 | "windows-targets", 770 | ] 771 | 772 | [[package]] 773 | name = "pkg-config" 774 | version = "0.3.32" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 777 | 778 | [[package]] 779 | name = "pretty_assertions" 780 | version = "1.4.1" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 783 | dependencies = [ 784 | "diff", 785 | "yansi", 786 | ] 787 | 788 | [[package]] 789 | name = "proc-macro2" 790 | version = "1.0.94" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 793 | dependencies = [ 794 | "unicode-ident", 795 | ] 796 | 797 | [[package]] 798 | name = "quote" 799 | version = "1.0.40" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 802 | dependencies = [ 803 | "proc-macro2", 804 | ] 805 | 806 | [[package]] 807 | name = "r-efi" 808 | version = "5.2.0" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 811 | 812 | [[package]] 813 | name = "redox_syscall" 814 | version = "0.2.16" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 817 | dependencies = [ 818 | "bitflags 1.3.2", 819 | ] 820 | 821 | [[package]] 822 | name = "redox_syscall" 823 | version = "0.5.10" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" 826 | dependencies = [ 827 | "bitflags 2.9.0", 828 | ] 829 | 830 | [[package]] 831 | name = "redox_users" 832 | version = "0.5.0" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 835 | dependencies = [ 836 | "getrandom 0.2.15", 837 | "libredox", 838 | "thiserror", 839 | ] 840 | 841 | [[package]] 842 | name = "regex" 843 | version = "1.11.1" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 846 | dependencies = [ 847 | "aho-corasick", 848 | "memchr", 849 | "regex-automata", 850 | "regex-syntax", 851 | ] 852 | 853 | [[package]] 854 | name = "regex-automata" 855 | version = "0.4.9" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 858 | dependencies = [ 859 | "aho-corasick", 860 | "memchr", 861 | "regex-syntax", 862 | ] 863 | 864 | [[package]] 865 | name = "regex-syntax" 866 | version = "0.8.5" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 869 | 870 | [[package]] 871 | name = "rusqlite" 872 | version = "0.34.0" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143" 875 | dependencies = [ 876 | "bitflags 2.9.0", 877 | "fallible-iterator", 878 | "fallible-streaming-iterator", 879 | "hashlink", 880 | "libsqlite3-sys", 881 | "smallvec", 882 | ] 883 | 884 | [[package]] 885 | name = "rustix" 886 | version = "0.38.44" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 889 | dependencies = [ 890 | "bitflags 2.9.0", 891 | "errno", 892 | "libc", 893 | "linux-raw-sys 0.4.15", 894 | "windows-sys", 895 | ] 896 | 897 | [[package]] 898 | name = "rustix" 899 | version = "1.0.5" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 902 | dependencies = [ 903 | "bitflags 2.9.0", 904 | "errno", 905 | "libc", 906 | "linux-raw-sys 0.9.3", 907 | "windows-sys", 908 | ] 909 | 910 | [[package]] 911 | name = "rustversion" 912 | version = "1.0.20" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 915 | 916 | [[package]] 917 | name = "ryu" 918 | version = "1.0.20" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 921 | 922 | [[package]] 923 | name = "scopeguard" 924 | version = "1.2.0" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 927 | 928 | [[package]] 929 | name = "serde" 930 | version = "1.0.219" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 933 | dependencies = [ 934 | "serde_derive", 935 | ] 936 | 937 | [[package]] 938 | name = "serde_derive" 939 | version = "1.0.219" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 942 | dependencies = [ 943 | "proc-macro2", 944 | "quote", 945 | "syn", 946 | ] 947 | 948 | [[package]] 949 | name = "serde_spanned" 950 | version = "0.6.8" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 953 | dependencies = [ 954 | "serde", 955 | ] 956 | 957 | [[package]] 958 | name = "shlex" 959 | version = "1.3.0" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 962 | 963 | [[package]] 964 | name = "sled" 965 | version = "0.34.7" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" 968 | dependencies = [ 969 | "crc32fast", 970 | "crossbeam-epoch", 971 | "crossbeam-utils", 972 | "fs2", 973 | "fxhash", 974 | "libc", 975 | "log", 976 | "parking_lot 0.11.2", 977 | ] 978 | 979 | [[package]] 980 | name = "smallvec" 981 | version = "1.14.0" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 984 | 985 | [[package]] 986 | name = "spin" 987 | version = "0.9.8" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 990 | dependencies = [ 991 | "lock_api", 992 | ] 993 | 994 | [[package]] 995 | name = "strsim" 996 | version = "0.11.1" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 999 | 1000 | [[package]] 1001 | name = "syn" 1002 | version = "2.0.100" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1005 | dependencies = [ 1006 | "proc-macro2", 1007 | "quote", 1008 | "unicode-ident", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "tempfile" 1013 | version = "3.19.1" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 1016 | dependencies = [ 1017 | "fastrand", 1018 | "getrandom 0.3.2", 1019 | "once_cell", 1020 | "rustix 1.0.5", 1021 | "windows-sys", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "thiserror" 1026 | version = "2.0.12" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1029 | dependencies = [ 1030 | "thiserror-impl", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "thiserror-impl" 1035 | version = "2.0.12" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1038 | dependencies = [ 1039 | "proc-macro2", 1040 | "quote", 1041 | "syn", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "toml" 1046 | version = "0.8.20" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 1049 | dependencies = [ 1050 | "serde", 1051 | "serde_spanned", 1052 | "toml_datetime", 1053 | "toml_edit", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "toml_datetime" 1058 | version = "0.6.8" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1061 | dependencies = [ 1062 | "serde", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "toml_edit" 1067 | version = "0.22.24" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 1070 | dependencies = [ 1071 | "indexmap", 1072 | "serde", 1073 | "serde_spanned", 1074 | "toml_datetime", 1075 | "winnow", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "unicode-ident" 1080 | version = "1.0.18" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1083 | 1084 | [[package]] 1085 | name = "unicode-segmentation" 1086 | version = "1.12.0" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1089 | 1090 | [[package]] 1091 | name = "unicode-width" 1092 | version = "0.2.0" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1095 | 1096 | [[package]] 1097 | name = "unty" 1098 | version = "0.0.4" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" 1101 | 1102 | [[package]] 1103 | name = "utf8parse" 1104 | version = "0.2.2" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1107 | 1108 | [[package]] 1109 | name = "uuid" 1110 | version = "1.16.0" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" 1113 | dependencies = [ 1114 | "getrandom 0.3.2", 1115 | "serde", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "vcpkg" 1120 | version = "0.2.15" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1123 | 1124 | [[package]] 1125 | name = "virtue" 1126 | version = "0.0.18" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" 1129 | 1130 | [[package]] 1131 | name = "wasi" 1132 | version = "0.11.0+wasi-snapshot-preview1" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1135 | 1136 | [[package]] 1137 | name = "wasi" 1138 | version = "0.14.2+wasi-0.2.4" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1141 | dependencies = [ 1142 | "wit-bindgen-rt", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "wasm-bindgen" 1147 | version = "0.2.100" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1150 | dependencies = [ 1151 | "cfg-if", 1152 | "once_cell", 1153 | "rustversion", 1154 | "wasm-bindgen-macro", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "wasm-bindgen-backend" 1159 | version = "0.2.100" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1162 | dependencies = [ 1163 | "bumpalo", 1164 | "log", 1165 | "proc-macro2", 1166 | "quote", 1167 | "syn", 1168 | "wasm-bindgen-shared", 1169 | ] 1170 | 1171 | [[package]] 1172 | name = "wasm-bindgen-macro" 1173 | version = "0.2.100" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1176 | dependencies = [ 1177 | "quote", 1178 | "wasm-bindgen-macro-support", 1179 | ] 1180 | 1181 | [[package]] 1182 | name = "wasm-bindgen-macro-support" 1183 | version = "0.2.100" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1186 | dependencies = [ 1187 | "proc-macro2", 1188 | "quote", 1189 | "syn", 1190 | "wasm-bindgen-backend", 1191 | "wasm-bindgen-shared", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "wasm-bindgen-shared" 1196 | version = "0.2.100" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1199 | dependencies = [ 1200 | "unicode-ident", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "winapi" 1205 | version = "0.3.9" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1208 | dependencies = [ 1209 | "winapi-i686-pc-windows-gnu", 1210 | "winapi-x86_64-pc-windows-gnu", 1211 | ] 1212 | 1213 | [[package]] 1214 | name = "winapi-i686-pc-windows-gnu" 1215 | version = "0.4.0" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1218 | 1219 | [[package]] 1220 | name = "winapi-x86_64-pc-windows-gnu" 1221 | version = "0.4.0" 1222 | source = "registry+https://github.com/rust-lang/crates.io-index" 1223 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1224 | 1225 | [[package]] 1226 | name = "windows" 1227 | version = "0.52.0" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" 1230 | dependencies = [ 1231 | "windows-core 0.52.0", 1232 | "windows-targets", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "windows-core" 1237 | version = "0.52.0" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1240 | dependencies = [ 1241 | "windows-targets", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "windows-core" 1246 | version = "0.61.0" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" 1249 | dependencies = [ 1250 | "windows-implement", 1251 | "windows-interface", 1252 | "windows-link", 1253 | "windows-result", 1254 | "windows-strings", 1255 | ] 1256 | 1257 | [[package]] 1258 | name = "windows-implement" 1259 | version = "0.60.0" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 1262 | dependencies = [ 1263 | "proc-macro2", 1264 | "quote", 1265 | "syn", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "windows-interface" 1270 | version = "0.59.1" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 1273 | dependencies = [ 1274 | "proc-macro2", 1275 | "quote", 1276 | "syn", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "windows-link" 1281 | version = "0.1.1" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 1284 | 1285 | [[package]] 1286 | name = "windows-result" 1287 | version = "0.3.2" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 1290 | dependencies = [ 1291 | "windows-link", 1292 | ] 1293 | 1294 | [[package]] 1295 | name = "windows-strings" 1296 | version = "0.4.0" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 1299 | dependencies = [ 1300 | "windows-link", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "windows-sys" 1305 | version = "0.59.0" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1308 | dependencies = [ 1309 | "windows-targets", 1310 | ] 1311 | 1312 | [[package]] 1313 | name = "windows-targets" 1314 | version = "0.52.6" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1317 | dependencies = [ 1318 | "windows_aarch64_gnullvm", 1319 | "windows_aarch64_msvc", 1320 | "windows_i686_gnu", 1321 | "windows_i686_gnullvm", 1322 | "windows_i686_msvc", 1323 | "windows_x86_64_gnu", 1324 | "windows_x86_64_gnullvm", 1325 | "windows_x86_64_msvc", 1326 | ] 1327 | 1328 | [[package]] 1329 | name = "windows_aarch64_gnullvm" 1330 | version = "0.52.6" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1333 | 1334 | [[package]] 1335 | name = "windows_aarch64_msvc" 1336 | version = "0.52.6" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1339 | 1340 | [[package]] 1341 | name = "windows_i686_gnu" 1342 | version = "0.52.6" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1345 | 1346 | [[package]] 1347 | name = "windows_i686_gnullvm" 1348 | version = "0.52.6" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1351 | 1352 | [[package]] 1353 | name = "windows_i686_msvc" 1354 | version = "0.52.6" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1357 | 1358 | [[package]] 1359 | name = "windows_x86_64_gnu" 1360 | version = "0.52.6" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1363 | 1364 | [[package]] 1365 | name = "windows_x86_64_gnullvm" 1366 | version = "0.52.6" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1369 | 1370 | [[package]] 1371 | name = "windows_x86_64_msvc" 1372 | version = "0.52.6" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1375 | 1376 | [[package]] 1377 | name = "winnow" 1378 | version = "0.7.4" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" 1381 | dependencies = [ 1382 | "memchr", 1383 | ] 1384 | 1385 | [[package]] 1386 | name = "wit-bindgen-rt" 1387 | version = "0.39.0" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1390 | dependencies = [ 1391 | "bitflags 2.9.0", 1392 | ] 1393 | 1394 | [[package]] 1395 | name = "yansi" 1396 | version = "1.0.1" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 1399 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hstdb" 3 | version = "3.0.0" 4 | authors = ["Alexander Thaller "] 5 | 6 | edition = "2024" 7 | rust-version = "1.85.0" 8 | 9 | description = "Better history management for zsh. Based on ideas from [https://github.com/larkery/zsh-histdb](https://github.com/larkery/zsh-histdb)." 10 | documentation = "https://docs.rs/hstdb/" 11 | homepage = "https://github.com/AlexanderThaller/hstdb" 12 | license = "MIT" 13 | readme = "README.md" 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [features] 18 | default = ["histdb-import"] 19 | histdb-import = ["rusqlite"] 20 | 21 | [dependencies] 22 | bincode = { version = "2", features = ["serde"] } 23 | chrono = { version = "0.4", features = ["serde"] } 24 | clap_complete = "4" 25 | clap = { version = "4", features = ["derive", "env"] } 26 | comfy-table = "7" 27 | crossbeam-utils = "0.8" 28 | csv = "1" 29 | ctrlc = { version = "3", features = ["termination"] } 30 | directories = "6" 31 | env_logger = { version = "0.11", default-features = false, features = ["color"] } 32 | exitcode = "1.1.2" 33 | flume = "0.11" 34 | glob = "0.3" 35 | hostname = "0.4" 36 | humantime = "2" 37 | log = { version = "0.4", features = ["serde"] } 38 | regex = "1" 39 | rusqlite = { version = "0.34", optional = true } 40 | serde = { version = "1", features = ["derive"] } 41 | sled = "0.34" 42 | thiserror = "2" 43 | toml = "0.8" 44 | uuid = { version = "1", features = ["serde", "v4"] } 45 | 46 | [dev-dependencies] 47 | tempfile = "3" 48 | pretty_assertions = "1" 49 | 50 | [profile.release] 51 | lto = true 52 | codegen-units = 1 53 | panic = "abort" 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2020] [Alexander Thaller] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hstdb 2 | 3 | [![Rust](https://github.com/AlexanderThaller/hstdb/actions/workflows/rust.yml/badge.svg)](https://github.com/AlexanderThaller/hstdb/actions/workflows/rust.yml) 4 | [![crates.io](https://img.shields.io/crates/v/hstdb.svg)](https://crates.io/crates/hstdb) 5 | 6 | Better history management for zsh. Based on ideas from 7 | [https://github.com/larkery/zsh-histdb](https://github.com/larkery/zsh-histdb). 8 | 9 | Licensed under MIT. 10 | 11 | It was mainly written because the sqlite merging broke a few to many times for 12 | me and using a sqlite database seemed overkill. 13 | 14 | The tool is just writing CSV files for each host which makes syncing them via 15 | git pretty painless. 16 | 17 | Has pretty much the same feature set as zsh-histdb: 18 | 19 | * Start and stop time of the command 20 | * Working directory in which the command was run 21 | * Hostname of the machine the command was run in 22 | * Unique session ids based on UUIDs 23 | * Exit status of the command 24 | * Import from zsh histfile and zsh-histdb sqlite database 25 | 26 | ## Installation 27 | 28 | You can either install the right binary from the releases page or run: 29 | 30 | ``` 31 | cargo install hstdb 32 | ``` 33 | 34 | ## Archlinux 35 | 36 | Install from AUR: 37 | * https://aur.archlinux.org/packages/hstdb/ 38 | * https://aur.archlinux.org/packages/hstdb-git/ 39 | 40 | ## First Start 41 | 42 | After you installed hstdb you need to start the server: 43 | 44 | ``` 45 | hstdb server 46 | ``` 47 | 48 | By default the server will run in the foreground. 49 | 50 | To stop the server you can run the following: 51 | 52 | ``` 53 | hstdb stop 54 | ``` 55 | 56 | Or send SIGTERM/SIGINT (Ctrl+C) to stop the server. 57 | 58 | You can also use the systemd unit file in 59 | [`hstdb.service`](resources/hstdb.service) which you can copy to 60 | `"$XDG_CONFIG_HOME/systemd` (usually `$HOME/.config/systemd`) and 61 | enable/start with the following: 62 | 63 | ``` 64 | systemctl --user daemon-reload 65 | systemctl --user enable hstdb.service 66 | systemctl --user start hstdb.service 67 | ``` 68 | 69 | After that you can add the following to your `.zshrc` to enable hstdb for 70 | you shell. 71 | 72 | ``` 73 | eval "$(hstdb init)" 74 | ``` 75 | 76 | You can run that in your current shell to enable hstdb or restart your 77 | shell. 78 | 79 | ## Usage 80 | 81 | Help output of default command: 82 | 83 | ``` 84 | hstdb 2.1.0 85 | Better history management for zsh. Based on ideas from 86 | [https://github.com/larkery/zsh-histdb](https://github.com/larkery/zsh-histdb). 87 | 88 | USAGE: 89 | hstdb [OPTIONS] [SUBCOMMAND] 90 | 91 | OPTIONS: 92 | --all-hosts 93 | Print all hosts 94 | 95 | -c, --command 96 | Only print entries beginning with the given command 97 | 98 | --config-path 99 | Path to the socket for communication with the server [env: HISTDBRS_CONFIG_PATH=] 100 | [default: $XDG_CONFIG_HOME/hstdb/config.toml] 101 | 102 | -d, --data-dir 103 | Path to folder in which to store the history files [default: 104 | $XDG_DATA_HOME/hstdb] 105 | 106 | --disable-formatting 107 | Disable fancy formatting 108 | 109 | -e, --entries-count 110 | How many entries to print [default: 25] 111 | 112 | -f, --folder 113 | Only print entries that have been executed in the given directory 114 | 115 | --filter-failed 116 | Filter out failed commands (return code not 0) 117 | 118 | --find-status 119 | Find commands with the given return code 120 | 121 | -h, --help 122 | Print help information 123 | 124 | --hide-header 125 | Disable printing of header 126 | 127 | --hostname 128 | Filter by given hostname 129 | 130 | -i, --in 131 | Only print entries that have been executed in the current directory 132 | 133 | --no-subdirs 134 | Exclude subdirectories when filtering by folder 135 | 136 | --session 137 | Filter by given session 138 | 139 | --show-duration 140 | Show how long the command ran 141 | 142 | --show-host 143 | Print host column 144 | 145 | --show-pwd 146 | Show directory in which the command was run 147 | 148 | --show-session 149 | Show session id for command 150 | 151 | --show-status 152 | Print returncode of command 153 | 154 | -t, --text 155 | Only print entries containing the given regex 156 | 157 | -T, --text_excluded 158 | Only print entries not containing the given regex 159 | 160 | -V, --version 161 | Print version information 162 | 163 | SUBCOMMANDS: 164 | bench 165 | Run benchmark against server 166 | completion 167 | Generate autocomplete files for shells 168 | disable 169 | Disable history recording for current session 170 | enable 171 | Enable history recording for current session 172 | help 173 | Print this message or the help of the given subcommand(s) 174 | import 175 | Import entries from existing histdb sqlite or zsh histfile 176 | init 177 | Print out shell functions needed by histdb and set current session id 178 | precmd 179 | Finish command for current session 180 | server 181 | Start the server 182 | session_id 183 | Get new session id 184 | stop 185 | Stop the server 186 | zshaddhistory 187 | Add new command for current session 188 | ``` 189 | 190 | The most basic command ist just running `hstdb` without any arguments: 191 | 192 | ``` 193 | » hstdb 194 | tmn cmd 195 | 14:28 cargo +nightly install --path . 196 | ``` 197 | 198 | That will print the history for the current machine. By default only the last 199 | 25 entries will be printed. 200 | 201 | ## Git 202 | 203 | hstdb was written to easily sync the history between multiple machines. For 204 | that hstdb will write separate history files for each machine. 205 | 206 | If you want to sync between machines go to the datadir (default is 207 | `$XDG_DATA_HOME/hstdb`) and run the following commands: 208 | 209 | ``` 210 | git init 211 | git add :/ 212 | git commit -m "Initial commit" 213 | ``` 214 | 215 | After that you can configure origins and start syncing the files between 216 | machines. There is no autocommit/autosync implemented as we don't want to have 217 | commits for each command run. This could be changed in the future. 218 | 219 | ## Configuration 220 | 221 | There is also a way to configure `hstdb`. By default the configuration 222 | is stored under `$XDG_CONFIG_HOME/hstdb/config.toml` (usually 223 | `$HOME/.config/hstdb/config.toml`). A different path can be specified 224 | using the `--config-path` option. 225 | 226 | The default configuration looks like this: 227 | 228 | ```toml 229 | # When true will not save commands that start with a space. 230 | # Default: true 231 | ignore_space = true 232 | 233 | # The log level to run under. 234 | # Default: Warn 235 | log_level = "Warn" 236 | ``` 237 | 238 | An example with all configuration options can be found in 239 | [config.toml](config.toml). 240 | 241 | ## Import 242 | 243 | ### zsh-histdb 244 | 245 | ``` 246 | » histdb import histdb -h 247 | hstdb-import-histdb 0.1.0 248 | Import entries from existing histdb sqlite file 249 | 250 | USAGE: 251 | hstdb import histdb [OPTIONS] 252 | 253 | FLAGS: 254 | -h, --help 255 | Prints help information 256 | 257 | 258 | OPTIONS: 259 | -d, --data-dir 260 | Path to folder in which to store the history files [default: $XDG_DATA_HOME/hstdb] 261 | 262 | -i, --import-file 263 | Path to the existing histdb sqlite file [default: $HOME/.histdb/zsh-history.db] 264 | ``` 265 | 266 | If the defaults for the `data-dir` and the `import-file` are fine you can just 267 | run the following command: 268 | 269 | ``` 270 | histdb import histdb 271 | ``` 272 | 273 | This will create CSV files for each `hostname` found in the sqlite database. It 274 | will create a UUID for each unique session found in sqlite so command run in the 275 | same session should still be grouped together. 276 | 277 | ### zsh histfile 278 | 279 | ``` 280 | » histdb import histfile -h 281 | hstdb-import-histfile 0.1.0 282 | Import entries from existing zsh histfile 283 | 284 | USAGE: 285 | hstdb import histfile [OPTIONS] 286 | 287 | FLAGS: 288 | -h, --help 289 | Prints help information 290 | 291 | 292 | OPTIONS: 293 | -d, --data-dir 294 | Path to folder in which to store the history files [default: $XDG_DATA_HOME/hstdb] 295 | 296 | -i, --import-file 297 | Path to the existing zsh histfile file [default: $HOME/.histfile] 298 | ``` 299 | 300 | If the defaults for the `data-dir` and the `import-file` are fine you can just 301 | run the following command: 302 | 303 | ``` 304 | histdb import histfile 305 | ``` 306 | 307 | As the information stored in the histfile is pretty limited the following 308 | information will be stored: 309 | 310 | * `time_finished` will be parsed from the histfile 311 | * `result` (exit code) will be parsed from the histfile 312 | * `command` will be parsed from the histfile 313 | * `time_start` will be copied over from `time_finished` 314 | * `hostname` will use the current machines hostname 315 | * `pwd` will be set to the current users home directory 316 | * `session_id` will be generated and used for all commands imported from the 317 | histfile 318 | * `user` will use the current user thats running the import 319 | 320 | ## Completion 321 | Currentyl only zsh generation is enabled as other shells don't make 322 | sense at the moment. 323 | 324 | Completion generation is provided through a subcommand: 325 | 326 | ``` 327 | » hstdb completion -h 328 | hstdb-completion 2.1.0 329 | Generate autocomplete files for shells 330 | 331 | USAGE: 332 | hstdb completion 333 | 334 | ARGS: 335 | 336 | For which shell to generate the autocomplete [default: zsh] [possible values: zsh] 337 | 338 | OPTIONS: 339 | -h, --help 340 | Print help information 341 | 342 | -V, --version 343 | Print version information 344 | ``` 345 | 346 | ### Zsh 347 | For zsh make sure your `$fpath` contains a folder you can write to: 348 | ``` 349 | # add .zsh_completion to load additional zsh stuff 350 | export fpath=(~/.zsh_completion $fpath) 351 | ``` 352 | 353 | Then write the autocomplete file to that folder: 354 | ``` 355 | hstdb completion zsh > ~/.zsh_completion/_hstdb 356 | ``` 357 | 358 | After that restart your shell which should now have working 359 | autocompletion. 360 | 361 | ## Contribution 362 | 363 | I'm happy with how the tool works for me so I won't expand it further but 364 | contributions for features and fixes are always welcome! 365 | 366 | ## Notes 367 | * This tool follows the [XDG Base Directory 368 | Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) 369 | where possible. 370 | 371 | 372 | ## Alternatives 373 | 374 | * https://github.com/ellie/atuin 375 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | # When true will not save commands that start with a space. 2 | # Default: true 3 | ignore_space = true 4 | 5 | # The log level to run under. 6 | # Default: Warn 7 | log_level = "Warn" 8 | 9 | # The hostname that should be used when writing an entry. If unset 10 | # will dynamically get the hostname from the system. 11 | # Default: None 12 | hostname = "thaller-desktop-linux" -------------------------------------------------------------------------------- /pkg/aur/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Alexander Thaller 2 | pkgname=hstdb 3 | pkgver=2.0.0 4 | pkgrel=1 5 | pkgdesc="Better history management for zsh." 6 | arch=('x86_64') 7 | url="https://github.com/AlexanderThaller/hstdb" 8 | license=('MIT') 9 | depends=('sqlite') 10 | makedepends=('cargo') 11 | source=("$pkgname-$pkgver.tar.gz::https://github.com/AlexanderThaller/$pkgname/archive/refs/tags/$pkgver.tar.gz") 12 | sha512sums=('f7153eba42c0e2e91e885e810953d60357cf205fdde808711968e05257e9c5b25ad9a3bec440e1ed460fdb0fdb3b28adbe3dbaa56610f3aa29ad2195ffdfb3a3') 13 | 14 | build() { 15 | cd "$pkgname-$pkgver" 16 | RUSTUP_TOOLCHAIN=stable cargo build --release --locked --all-features --target-dir=target 17 | } 18 | 19 | check() { 20 | cd "$pkgname-$pkgver" 21 | RUSTUP_TOOLCHAIN=stable cargo test --release --locked --target-dir=target 22 | } 23 | 24 | package() { 25 | cd "$pkgname-$pkgver" 26 | install -Dm 755 target/release/${pkgname} -t "${pkgdir}/usr/bin" 27 | } 28 | -------------------------------------------------------------------------------- /resources/hstdb.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Start hstdb server 3 | 4 | [Service] 5 | ExecStart=%h/.cargo/bin/hstdb server 6 | ExecStop=%h/.cargo/bin/hstdb stop 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /resources/init.zsh: -------------------------------------------------------------------------------- 1 | function hstdb-init() { 2 | local session_id; 3 | session_id="$(hstdb session_id)" 4 | export HISTDB_RS_SESSION_ID="${session_id}" 5 | } 6 | 7 | function hstdb-zshaddhistory() { 8 | unset HISTDB_RS_RETVAL; 9 | hstdb zshaddhistory $@ 10 | } 11 | 12 | function hstdb-precmd() { 13 | export HISTDB_RS_RETVAL="${?}" 14 | hstdb precmd 15 | } 16 | 17 | autoload -Uz add-zsh-hook 18 | 19 | add-zsh-hook zshaddhistory hstdb-zshaddhistory 20 | add-zsh-hook precmd hstdb-precmd 21 | 22 | hstdb-init 23 | -------------------------------------------------------------------------------- /resources/send_test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export RUN_DIR="/tmp/tmp.4y3mPdtiZB-tmpdir" 4 | ./target/release/hstdb zshaddhistory -s "${RUN_DIR}/socket" "test" 5 | export HISTDB_RS_RETVAL=0 6 | ./target/release/hstdb precmd -s "${RUN_DIR}/socket" 7 | -------------------------------------------------------------------------------- /resources/start_test_server: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export RUN_DIR="/tmp/tmp.4y3mPdtiZB-tmpdir" 4 | ./target/release/hstdb server -c "${RUN_DIR}/cache" -d "${RUN_DIR}/datadir" -s "${RUN_DIR}/socket" 5 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use bincode::serde::BorrowCompat; 2 | use std::{ 3 | os::unix::net::UnixDatagram, 4 | path::PathBuf, 5 | }; 6 | use thiserror::Error; 7 | 8 | use crate::message::Message; 9 | 10 | #[derive(Debug)] 11 | pub struct Client { 12 | socket_path: PathBuf, 13 | } 14 | 15 | #[derive(Error, Debug)] 16 | pub enum Error { 17 | #[error("can not create socket: {0}")] 18 | CreateSocket(std::io::Error), 19 | 20 | #[error("can not connect socket: {0}")] 21 | ConnectSocket(std::io::Error), 22 | 23 | #[error("can not serialize message: {0}")] 24 | SerializeMessage(bincode::error::EncodeError), 25 | 26 | #[error("can not send message to socket: {0}")] 27 | SendMessage(std::io::Error), 28 | } 29 | 30 | pub const fn new(socket_path: PathBuf) -> Client { 31 | Client { socket_path } 32 | } 33 | 34 | impl Client { 35 | pub fn send(&self, message: &Message) -> Result<(), Error> { 36 | let socket = UnixDatagram::unbound().map_err(Error::CreateSocket)?; 37 | 38 | socket 39 | .connect(&self.socket_path) 40 | .map_err(Error::ConnectSocket)?; 41 | 42 | let data = bincode::encode_to_vec(BorrowCompat(message), bincode::config::standard()) 43 | .map_err(Error::SerializeMessage)?; 44 | 45 | socket.send(&data).map_err(Error::SendMessage)?; 46 | 47 | Ok(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use log::{ 2 | LevelFilter, 3 | debug, 4 | }; 5 | use std::path::Path; 6 | use thiserror::Error; 7 | 8 | use serde::Deserialize; 9 | 10 | #[derive(Debug, Error)] 11 | pub enum Error { 12 | #[error("can not read config file: {0}")] 13 | ReadFile(std::io::Error), 14 | 15 | #[error("can not parse config file: {0}")] 16 | ParseConfig(toml::de::Error), 17 | } 18 | 19 | #[derive(Debug, Deserialize)] 20 | #[serde(default)] 21 | pub struct Config { 22 | /// Then true disables recording commands that start with a space. 23 | pub ignore_space: bool, 24 | 25 | /// The log level to run under. 26 | pub log_level: LevelFilter, 27 | 28 | /// The hostname that should be used when writing an entry. If 29 | /// unset will dynamically get the hostname from the system. 30 | pub hostname: Option, 31 | } 32 | 33 | impl Default for Config { 34 | fn default() -> Self { 35 | Self { 36 | ignore_space: true, 37 | log_level: LevelFilter::Warn, 38 | hostname: None, 39 | } 40 | } 41 | } 42 | 43 | impl Config { 44 | pub fn open(path: impl AsRef) -> Result { 45 | if !path.as_ref().is_file() { 46 | debug!("no config file found using default"); 47 | return Ok(Self::default()); 48 | } 49 | 50 | let config_data = std::fs::read_to_string(path).map_err(Error::ReadFile)?; 51 | let config = toml::de::from_str(&config_data).map_err(Error::ParseConfig)?; 52 | 53 | Ok(config) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/entry.rs: -------------------------------------------------------------------------------- 1 | use crate::message::{ 2 | CommandFinished, 3 | CommandStart, 4 | }; 5 | use chrono::{ 6 | DateTime, 7 | Utc, 8 | }; 9 | use serde::{ 10 | Deserialize, 11 | Serialize, 12 | }; 13 | use std::path::PathBuf; 14 | use uuid::Uuid; 15 | 16 | #[derive(Debug, Serialize, Deserialize, Ord, PartialOrd, PartialEq, Eq)] 17 | pub struct Entry { 18 | pub time_finished: DateTime, 19 | pub time_start: DateTime, 20 | pub hostname: String, 21 | pub command: String, 22 | pub pwd: PathBuf, 23 | pub result: u16, 24 | pub session_id: Uuid, 25 | pub user: String, 26 | } 27 | 28 | impl Entry { 29 | pub fn from_messages(start: CommandStart, finish: &CommandFinished) -> Self { 30 | let command = start.command.trim_end(); 31 | 32 | let command = command 33 | .strip_suffix("\\r\\n") 34 | .or_else(|| command.strip_suffix("\\n")) 35 | .unwrap_or(command) 36 | .to_string(); 37 | 38 | let user = start.user.trim().to_string(); 39 | let hostname = start.hostname.trim().to_string(); 40 | 41 | Self { 42 | time_finished: finish.time_stamp, 43 | time_start: start.time_stamp, 44 | hostname, 45 | command, 46 | pwd: start.pwd, 47 | result: finish.result, 48 | session_id: start.session_id, 49 | user, 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod config; 3 | pub mod entry; 4 | pub mod message; 5 | pub mod opt; 6 | pub mod run; 7 | pub mod server; 8 | pub mod store; 9 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![warn(clippy::allow_attributes)] 3 | #![warn(clippy::allow_attributes_without_reason)] 4 | #![warn(clippy::dbg_macro)] 5 | #![warn(clippy::pedantic)] 6 | #![warn(clippy::unwrap_used)] 7 | #![warn(rust_2018_idioms, unused_lifetimes, missing_debug_implementations)] 8 | 9 | use clap::Parser; 10 | 11 | mod client; 12 | mod config; 13 | mod entry; 14 | mod message; 15 | mod opt; 16 | mod run; 17 | mod server; 18 | mod store; 19 | 20 | use log::error; 21 | use opt::Opt; 22 | 23 | fn main() { 24 | let opt = Opt::parse(); 25 | 26 | match opt.run() { 27 | Err(run::Error::WriteStdout(io_err)) => { 28 | // If pipe is closed we can savely ignore that error 29 | if io_err.kind() == std::io::ErrorKind::BrokenPipe {} 30 | } 31 | 32 | Err(err) => error!("{err}"), 33 | 34 | Ok(()) => (), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/message.rs: -------------------------------------------------------------------------------- 1 | use chrono::{ 2 | DateTime, 3 | Utc, 4 | }; 5 | use std::{ 6 | env, 7 | path::PathBuf, 8 | }; 9 | use thiserror::Error; 10 | use uuid::Uuid; 11 | 12 | use crate::config::Config; 13 | 14 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 15 | pub enum Message { 16 | Stop, 17 | Disable(Uuid), 18 | Enable(Uuid), 19 | CommandStart(CommandStart), 20 | CommandFinished(CommandFinished), 21 | } 22 | 23 | #[derive(Error, Debug)] 24 | pub enum Error { 25 | #[error("can not get hostname: {0}")] 26 | GetHostname(std::io::Error), 27 | 28 | #[error("can not get current directory: {0}")] 29 | GetCurrentDir(std::io::Error), 30 | 31 | #[error("can not get current user: {0}")] 32 | GetUser(env::VarError), 33 | 34 | #[error("invalid session id in environment variable: {0}")] 35 | InvalidSessionIDEnvVar(env::VarError), 36 | 37 | #[error("invalid session id: {0}")] 38 | InvalidSessionID(uuid::Error), 39 | 40 | #[error("session id is missing")] 41 | MissingSessionID, 42 | 43 | #[error("retval is missing")] 44 | MissingRetval(std::env::VarError), 45 | 46 | #[error("invalid result: {0}")] 47 | InvalidResult(std::num::ParseIntError), 48 | } 49 | 50 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 51 | pub struct CommandStart { 52 | pub command: String, 53 | pub pwd: PathBuf, 54 | pub session_id: Uuid, 55 | pub time_stamp: DateTime, 56 | pub user: String, 57 | pub hostname: String, 58 | } 59 | 60 | impl CommandStart { 61 | pub fn from_env(config: &Config, command: String) -> Result { 62 | let pwd = env::current_dir().map_err(Error::GetCurrentDir)?; 63 | 64 | let time_stamp = Utc::now(); 65 | 66 | let user = env::var("USER").map_err(Error::GetUser)?; 67 | 68 | let session_id = session_id_from_env()?; 69 | 70 | let hostname = if let Some(hostname) = config.hostname.clone() { 71 | hostname 72 | } else { 73 | hostname::get() 74 | .map_err(Error::GetHostname)? 75 | .to_string_lossy() 76 | .to_string() 77 | }; 78 | 79 | Ok(Self { 80 | command, 81 | pwd, 82 | session_id, 83 | time_stamp, 84 | user, 85 | hostname, 86 | }) 87 | } 88 | } 89 | 90 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 91 | pub struct CommandFinished { 92 | pub session_id: Uuid, 93 | pub time_stamp: DateTime, 94 | pub result: u16, 95 | } 96 | 97 | impl CommandFinished { 98 | pub fn from_env() -> Result { 99 | let time_stamp = Utc::now(); 100 | 101 | let session_id = session_id_from_env()?; 102 | 103 | let result = env::var("HISTDB_RS_RETVAL") 104 | .map_err(Error::MissingRetval)? 105 | .parse() 106 | .map_err(Error::InvalidResult)?; 107 | 108 | Ok(Self { 109 | session_id, 110 | time_stamp, 111 | result, 112 | }) 113 | } 114 | } 115 | 116 | pub fn session_id_from_env() -> Result { 117 | match env::var("HISTDB_RS_SESSION_ID") { 118 | Err(err) => match err { 119 | env::VarError::NotPresent => Err(Error::MissingSessionID), 120 | env::VarError::NotUnicode(_) => Err(Error::InvalidSessionIDEnvVar(err)), 121 | }, 122 | 123 | Ok(s) => Uuid::parse_str(&s).map_err(Error::InvalidSessionID), 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/opt.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{ 4 | CommandFactory, 5 | Parser, 6 | Subcommand, 7 | }; 8 | use directories::{ 9 | BaseDirs, 10 | ProjectDirs, 11 | }; 12 | use log::error; 13 | use regex::Regex; 14 | use thiserror::Error; 15 | 16 | use crate::{ 17 | config, 18 | run, 19 | run::{ 20 | Display, 21 | TableDisplay, 22 | }, 23 | store::Filter, 24 | }; 25 | 26 | #[derive(Error, Debug)] 27 | pub enum Error { 28 | #[error("can not get base directories")] 29 | BaseDirectory, 30 | 31 | #[error("can not get project dirs")] 32 | ProjectDirs, 33 | } 34 | 35 | fn project_dir() -> ProjectDirs { 36 | ProjectDirs::from("com", "hstdb", "hstdb") 37 | .ok_or(Error::ProjectDirs) 38 | .expect("can not get project_dir") 39 | } 40 | 41 | fn base_directory() -> BaseDirs { 42 | directories::BaseDirs::new() 43 | .ok_or(Error::BaseDirectory) 44 | .expect("can not get base directory") 45 | } 46 | 47 | fn default_data_dir() -> PathBuf { 48 | let project_dir = project_dir(); 49 | let data_dir = project_dir.data_dir(); 50 | 51 | data_dir.to_owned() 52 | } 53 | 54 | fn default_cache_path() -> PathBuf { 55 | let project_dir = project_dir(); 56 | project_dir.cache_dir().join("server") 57 | } 58 | 59 | fn default_histdb_sqlite_path() -> PathBuf { 60 | let base_dirs = base_directory(); 61 | let home = base_dirs.home_dir(); 62 | home.join(".histdb").join("zsh-history.db") 63 | } 64 | 65 | fn default_zsh_histfile_path() -> PathBuf { 66 | let base_dirs = base_directory(); 67 | let home = base_dirs.home_dir(); 68 | home.join(".histfile") 69 | } 70 | 71 | fn default_socket_path() -> PathBuf { 72 | let project_dir = project_dir(); 73 | 74 | let fallback_path = PathBuf::from("/tmp/hstdb/"); 75 | 76 | project_dir 77 | .runtime_dir() 78 | .unwrap_or(&fallback_path) 79 | .join("server_socket") 80 | } 81 | 82 | fn default_config_path() -> PathBuf { 83 | let project_dir = project_dir(); 84 | 85 | project_dir.config_dir().join("config.toml") 86 | } 87 | 88 | #[derive(Parser, Debug)] 89 | struct ZSHAddHistory { 90 | #[clap(flatten)] 91 | socket_path: Socket, 92 | 93 | /// Command to add to history 94 | #[clap(index = 1)] 95 | command: String, 96 | } 97 | 98 | #[derive(Parser, Debug)] 99 | struct Server { 100 | /// Path to the cachefile used to store entries between restarts 101 | #[clap(short, long, default_value_os_t = default_cache_path())] 102 | cache_path: PathBuf, 103 | 104 | #[clap(flatten)] 105 | data_dir: DataDir, 106 | 107 | #[clap(flatten)] 108 | socket_path: Socket, 109 | } 110 | 111 | #[derive(Subcommand, Debug)] 112 | enum Import { 113 | #[cfg(feature = "histdb-import")] 114 | /// Import entries from existing histdb sqlite file 115 | Histdb(ImportHistdb), 116 | 117 | /// Import entries from existing zsh histfile 118 | Histfile(ImportHistfile), 119 | } 120 | 121 | #[derive(Parser, Debug)] 122 | struct ImportHistdb { 123 | #[clap(flatten)] 124 | data_dir: DataDir, 125 | 126 | /// Path to the existing histdb sqlite file 127 | #[clap(short, long, default_value_os_t = default_histdb_sqlite_path())] 128 | import_file: PathBuf, 129 | } 130 | 131 | #[derive(Parser, Debug)] 132 | struct ImportHistfile { 133 | #[clap(flatten)] 134 | data_dir: DataDir, 135 | 136 | /// Path to the existing zsh histfile file 137 | #[clap(short, long, default_value_os_t = default_zsh_histfile_path())] 138 | import_file: PathBuf, 139 | } 140 | 141 | #[derive(Parser, Debug)] 142 | struct Socket { 143 | /// Path to the socket for communication with the server 144 | #[clap(short, long, env = "HISTDBRS_SOCKET_PATH", default_value_os_t = default_socket_path())] 145 | socket_path: PathBuf, 146 | } 147 | 148 | #[derive(Parser, Debug)] 149 | struct Config { 150 | /// Path to the socket for communication with the server 151 | #[clap(long, env = "HISTDBRS_CONFIG_PATH", default_value_os_t = default_config_path())] 152 | config_path: PathBuf, 153 | } 154 | 155 | #[derive(Parser, Debug)] 156 | struct DataDir { 157 | /// Path to folder in which to store the history files 158 | #[clap( 159 | short, 160 | long, 161 | default_value_os_t = default_data_dir() 162 | )] 163 | data_dir: PathBuf, 164 | } 165 | 166 | #[expect( 167 | clippy::struct_excessive_bools, 168 | reason = "this is a cli app and its fine if there are a lot of bools" 169 | )] 170 | #[derive(Parser, Debug)] 171 | struct DefaultArgs { 172 | #[clap(flatten)] 173 | data_dir: DataDir, 174 | 175 | /// How many entries to print 176 | #[clap(short, long, default_value = "25")] 177 | entries_count: usize, 178 | 179 | /// Only print entries beginning with the given command 180 | #[clap(short, long)] 181 | command: Option, 182 | 183 | /// Only print entries containing the given regex 184 | #[clap(short = 't', long = "text")] 185 | command_text: Option, 186 | 187 | /// Only print entries not containing the given regex 188 | #[clap(short = 'T', long = "text-excluded", alias = "text_excluded")] 189 | command_text_excluded: Option, 190 | 191 | /// Only print entries that have been executed in the current directory 192 | #[clap(short, long = "in", conflicts_with = "folder")] 193 | in_current: bool, 194 | 195 | /// Only print entries that have been executed in the given directory 196 | #[clap(short, long)] 197 | folder: Option, 198 | 199 | /// Exclude subdirectories when filtering by folder 200 | #[clap(long)] 201 | no_subdirs: bool, 202 | 203 | /// Filter by given hostname 204 | #[clap(long, conflicts_with = "all_hosts")] 205 | hostname: Option, 206 | 207 | /// Filter by given session 208 | #[clap(long)] 209 | session: Option, 210 | 211 | /// Print all hosts 212 | #[clap(long)] 213 | all_hosts: bool, 214 | 215 | /// Disable fancy formatting 216 | #[clap(long)] 217 | disable_formatting: bool, 218 | 219 | /// Print host column 220 | #[clap(long)] 221 | show_host: bool, 222 | 223 | /// Print returncode of command 224 | #[clap(long)] 225 | show_status: bool, 226 | 227 | /// Show how long the command ran 228 | #[clap(long)] 229 | show_duration: bool, 230 | 231 | /// Show directory in which the command was run 232 | #[clap(long)] 233 | show_pwd: bool, 234 | 235 | /// Show session id for command 236 | #[clap(long)] 237 | show_session: bool, 238 | 239 | /// Disable printing of header 240 | #[clap(long)] 241 | hide_header: bool, 242 | 243 | /// Filter out failed commands (return code not 0) 244 | #[clap(long)] 245 | filter_failed: bool, 246 | 247 | /// Find commands with the given return code 248 | #[clap(long)] 249 | find_status: Option, 250 | 251 | #[clap(flatten)] 252 | config: Config, 253 | } 254 | 255 | #[derive(Subcommand, Debug)] 256 | enum SubCommand { 257 | /// Add new command for current session 258 | #[clap(name = "zshaddhistory")] 259 | ZSHAddHistory(ZSHAddHistory), 260 | 261 | /// Start the server 262 | #[clap(name = "server")] 263 | Server(Server), 264 | 265 | /// Stop the server 266 | #[clap(name = "stop")] 267 | Stop(Socket), 268 | 269 | /// Disable history recording for current session 270 | #[clap(name = "disable")] 271 | Disable(Socket), 272 | 273 | /// Enable history recording for current session 274 | #[clap(name = "enable")] 275 | Enable(Socket), 276 | 277 | /// Finish command for current session 278 | #[clap(name = "precmd")] 279 | PreCmd(Socket), 280 | 281 | /// Get new session id 282 | #[clap(name = "session_id")] 283 | SessionID, 284 | 285 | /// Import entries from existing histdb sqlite or zsh histfile 286 | #[clap(subcommand, name = "import")] 287 | Import(Import), 288 | 289 | /// Print out shell functions needed by histdb and set current session id 290 | #[clap(name = "init")] 291 | Init, 292 | 293 | /// Run benchmark against server 294 | #[clap(name = "bench")] 295 | Bench(Socket), 296 | 297 | /// Generate autocomplete files for shells 298 | #[clap(name = "completion")] 299 | Completion(CompletionOpts), 300 | } 301 | 302 | #[derive(Parser, Debug)] 303 | pub struct CompletionOpts { 304 | /// For which shell to generate the autocomplete 305 | #[clap(value_parser, default_value = "zsh")] 306 | shell: clap_complete::Shell, 307 | } 308 | 309 | #[derive(Parser, Debug)] 310 | #[clap(version, about)] 311 | pub struct Opt { 312 | #[clap(flatten)] 313 | default_args: DefaultArgs, 314 | 315 | #[clap(subcommand)] 316 | sub_command: Option, 317 | } 318 | 319 | impl Opt { 320 | #[expect(clippy::result_large_err, reason = "we will fix this if we need to")] 321 | pub fn run(self) -> Result<(), run::Error> { 322 | let sub_command = self.sub_command; 323 | let in_current = self.default_args.in_current; 324 | let folder = self.default_args.folder; 325 | let all_hosts = self.default_args.all_hosts; 326 | let hostname = self.default_args.hostname; 327 | let data_dir = self.default_args.data_dir.data_dir; 328 | let entries_count = self.default_args.entries_count; 329 | let command = self.default_args.command; 330 | let session_filter = self.default_args.session; 331 | let no_subdirs = self.default_args.no_subdirs; 332 | let command_text = self.default_args.command_text; 333 | let command_text_excluded = self.default_args.command_text_excluded; 334 | let filter_failed = self.default_args.filter_failed; 335 | let find_status = self.default_args.find_status; 336 | let config = config::Config::open(self.default_args.config.config_path) 337 | .map_err(run::Error::ReadConfig)?; 338 | 339 | let format = !self.default_args.disable_formatting; 340 | let duration = Display::should_show(self.default_args.show_duration); 341 | let header = Display::should_hide(self.default_args.hide_header); 342 | let host = Display::should_show(self.default_args.show_host); 343 | let pwd = Display::should_show(self.default_args.show_pwd); 344 | let session = Display::should_show(self.default_args.show_session); 345 | let status = Display::should_show(self.default_args.show_status); 346 | 347 | env_logger::init(); 348 | 349 | sub_command.map_or_else( 350 | || { 351 | let filter = Filter::default() 352 | .directory(folder, in_current, no_subdirs)? 353 | .hostname(hostname, all_hosts)? 354 | .count(entries_count) 355 | .command(command, command_text, command_text_excluded) 356 | .session(session_filter) 357 | .filter_failed(filter_failed) 358 | .find_status(find_status); 359 | 360 | let display = TableDisplay { 361 | format, 362 | 363 | duration, 364 | header, 365 | host, 366 | pwd, 367 | session, 368 | status, 369 | }; 370 | 371 | run::default(&filter, &display, data_dir) 372 | }, 373 | |sub_command| match sub_command { 374 | SubCommand::ZSHAddHistory(o) => { 375 | run::zsh_add_history(&config, o.command, o.socket_path.socket_path) 376 | } 377 | SubCommand::Server(o) => { 378 | run::server(o.cache_path, o.socket_path.socket_path, o.data_dir.data_dir) 379 | } 380 | SubCommand::Stop(o) => run::stop(o.socket_path), 381 | SubCommand::Disable(o) => run::disable(o.socket_path), 382 | SubCommand::Enable(o) => run::enable(o.socket_path), 383 | SubCommand::PreCmd(o) => run::precmd(o.socket_path), 384 | SubCommand::SessionID => { 385 | run::session_id(); 386 | Ok(()) 387 | } 388 | SubCommand::Import(s) => match s { 389 | #[cfg(feature = "histdb-import")] 390 | Import::Histdb(o) => run::import::histdb(&o.import_file, o.data_dir.data_dir) 391 | .map_err(run::Error::ImportHistdb), 392 | Import::Histfile(o) => { 393 | run::import::histfile(&o.import_file, o.data_dir.data_dir) 394 | .map_err(run::Error::ImportHistfile) 395 | } 396 | }, 397 | SubCommand::Init => { 398 | run::init(); 399 | Ok(()) 400 | } 401 | SubCommand::Bench(s) => run::bench(s.socket_path), 402 | SubCommand::Completion(o) => { 403 | let mut cmd = Opt::command(); 404 | let name = cmd.get_name().to_string(); 405 | 406 | clap_complete::generate(o.shell, &mut cmd, name, &mut std::io::stdout()); 407 | 408 | Ok(()) 409 | } 410 | }, 411 | ) 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/run/import.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client, 3 | message, 4 | server, 5 | store, 6 | }; 7 | use chrono::{ 8 | DateTime, 9 | Utc, 10 | }; 11 | #[cfg(feature = "histdb-import")] 12 | use log::info; 13 | use log::warn; 14 | #[cfg(feature = "histdb-import")] 15 | use rusqlite::params; 16 | #[cfg(feature = "histdb-import")] 17 | use std::convert::TryInto; 18 | use std::{ 19 | io::BufRead, 20 | path::{ 21 | Path, 22 | PathBuf, 23 | }, 24 | }; 25 | use thiserror::Error; 26 | use uuid::Uuid; 27 | 28 | #[derive(Error, Debug)] 29 | pub enum Error { 30 | #[error("{0}")] 31 | Client(#[from] client::Error), 32 | 33 | #[error("{0}")] 34 | Message(#[from] message::Error), 35 | 36 | #[error("{0}")] 37 | Server(#[from] server::Error), 38 | 39 | #[error("{0}")] 40 | Store(#[from] store::Error), 41 | 42 | #[error("can not get hostname: {0}")] 43 | GetHostname(std::io::Error), 44 | 45 | #[cfg(feature = "histdb-import")] 46 | #[error("can not open sqlite database: {0}")] 47 | OpenSqliteDatabase(rusqlite::Error), 48 | 49 | #[cfg(feature = "histdb-import")] 50 | #[error("can not prepare sqlite query to get entries: {0}")] 51 | PrepareSqliteQuery(rusqlite::Error), 52 | 53 | #[cfg(feature = "histdb-import")] 54 | #[error("can not convert sqlite row: {0}")] 55 | ConvertSqliteRow(rusqlite::Error), 56 | 57 | #[cfg(feature = "histdb-import")] 58 | #[error("can not collect entries from sqlite query: {0}")] 59 | CollectEntries(rusqlite::Error), 60 | 61 | #[cfg(feature = "histdb-import")] 62 | #[error("can not convert exit status from sqlite: {0}")] 63 | ConvertExitStatus(std::num::TryFromIntError), 64 | 65 | #[error("can not open histfile: {0}")] 66 | OpenHistfile(std::io::Error), 67 | 68 | #[error("accumulator fortime finished is none")] 69 | TimeFinishedAccumulatorNone, 70 | 71 | #[error("accumulator for result is none")] 72 | ResultAccumulatorNone, 73 | 74 | #[error("accumulator for command is none")] 75 | CommandAccumulatorNone, 76 | 77 | #[error("did not find timestamp in histfile line {0}")] 78 | NoTimestamp(usize), 79 | 80 | #[error("did not find result code in histfile line {0}")] 81 | NoCode(usize), 82 | 83 | #[error("can not parse timestamp as number from histfile line {1}: {0}")] 84 | ParseTimestamp(std::num::ParseIntError, usize), 85 | 86 | #[error("can not parse returncode from histfile line {1}: {0}")] 87 | ParseResultCode(std::num::ParseIntError, usize), 88 | 89 | #[error("can not get base directories")] 90 | BaseDirectory, 91 | 92 | #[error("can not get current user: {0}")] 93 | GetUser(std::env::VarError), 94 | 95 | #[error("time start is missing")] 96 | TimeStartMissing, 97 | 98 | #[error("time finished is missing")] 99 | TimeFinishedMissing, 100 | } 101 | 102 | #[cfg(feature = "histdb-import")] 103 | pub fn histdb(import_file: impl AsRef, data_dir: PathBuf) -> Result<(), Error> { 104 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] 105 | struct DBEntry { 106 | session: i64, 107 | start_time: i64, 108 | duration: Option, 109 | exit_status: Option, 110 | hostname: String, 111 | pwd: String, 112 | command: String, 113 | } 114 | 115 | let db = rusqlite::Connection::open(&import_file).map_err(Error::OpenSqliteDatabase)?; 116 | 117 | let mut stmt = db 118 | .prepare( 119 | "select * from history left join places on places.id=history.place_id 120 | left join commands on history.command_id=commands.id", 121 | ) 122 | .map_err(Error::PrepareSqliteQuery)?; 123 | 124 | let entries = stmt 125 | .query_map(params![], |row| { 126 | Ok(DBEntry { 127 | session: row.get(1)?, 128 | exit_status: row.get(4)?, 129 | start_time: row.get(5)?, 130 | duration: row.get(6)?, 131 | hostname: row.get(8)?, 132 | pwd: row.get(9)?, 133 | command: row.get(11)?, 134 | }) 135 | }) 136 | .map_err(Error::ConvertSqliteRow)? 137 | .collect::, _>>() 138 | .map_err(Error::CollectEntries)?; 139 | 140 | info!("importing {:?} entries", entries.len()); 141 | 142 | let mut session_ids = std::collections::HashMap::new(); 143 | 144 | let store = crate::store::new(data_dir); 145 | 146 | for entry in entries { 147 | if entry.duration.is_none() 148 | || entry.exit_status.is_none() 149 | || entry.command.trim().is_empty() 150 | { 151 | continue; 152 | } 153 | 154 | let session_id = session_ids 155 | .entry((entry.session, entry.hostname.clone())) 156 | .or_insert_with(Uuid::new_v4); 157 | 158 | let start_time = entry.start_time; 159 | 160 | let time_start = chrono::DateTime::::from_timestamp(start_time, 0) 161 | .ok_or(Error::TimeStartMissing)?; 162 | 163 | let time_finished = chrono::DateTime::::from_timestamp( 164 | start_time 165 | + entry 166 | .duration 167 | .expect("save as we already checked if duration is some earlier"), 168 | 0, 169 | ) 170 | .ok_or(Error::TimeFinishedMissing)?; 171 | 172 | let hostname = entry.hostname; 173 | let pwd = PathBuf::from(entry.pwd); 174 | let result = entry 175 | .exit_status 176 | .expect("save as we already checked if status is some earlier") 177 | .try_into() 178 | .map_err(Error::ConvertExitStatus)?; 179 | 180 | let user = String::new(); 181 | let command = entry.command; 182 | 183 | let entry = crate::entry::Entry { 184 | time_finished, 185 | time_start, 186 | hostname, 187 | pwd, 188 | result, 189 | session_id: *session_id, 190 | user, 191 | command, 192 | }; 193 | 194 | store.add_entry(&entry)?; 195 | } 196 | 197 | Ok(()) 198 | } 199 | 200 | #[expect( 201 | clippy::too_many_lines, 202 | reason = "this function is too long and we should split it up" 203 | )] 204 | pub fn histfile(import_file: impl AsRef, data_dir: PathBuf) -> Result<(), Error> { 205 | #[derive(Debug)] 206 | struct HistfileEntry { 207 | time_finished: DateTime, 208 | result: u16, 209 | command: String, 210 | } 211 | 212 | let histfile = std::fs::File::open(import_file).map_err(Error::OpenHistfile)?; 213 | let reader = std::io::BufReader::new(histfile); 214 | 215 | let mut acc_time_finished: Option> = None; 216 | let mut acc_result: Option = None; 217 | let mut acc_command: Option = None; 218 | let mut multiline_command = false; 219 | 220 | let mut entries = Vec::new(); 221 | 222 | for (index, line) in reader.lines().enumerate() { 223 | let line_number = index + 1; 224 | 225 | let line = match line { 226 | Err(err) => { 227 | warn!("can not read line {line_number}: {err}"); 228 | 229 | continue; 230 | } 231 | Ok(line) => line, 232 | }; 233 | 234 | // End of multiline command 235 | if line.starts_with(':') && multiline_command { 236 | let time_finished = acc_time_finished.ok_or(Error::TimeFinishedAccumulatorNone)?; 237 | let result = acc_result.ok_or(Error::ResultAccumulatorNone)?; 238 | let command = acc_command.ok_or(Error::CommandAccumulatorNone)?; 239 | 240 | acc_time_finished = None; 241 | acc_result = None; 242 | acc_command = None; 243 | multiline_command = false; 244 | 245 | entries.push(HistfileEntry { 246 | time_finished, 247 | result, 248 | command, 249 | }); 250 | } 251 | 252 | if line.starts_with(':') { 253 | let mut split = line.split(':'); 254 | 255 | let timestamp = split.nth(1).ok_or(Error::NoTimestamp(line_number))?.trim(); 256 | 257 | let code_command = split.collect::>().join(":"); 258 | let mut code_command = code_command.split(';'); 259 | 260 | let code = code_command.next().ok_or(Error::NoCode(line_number))?; 261 | 262 | let command = code_command.collect::>().join(";"); 263 | 264 | let time_finished = chrono::DateTime::::from_timestamp( 265 | timestamp 266 | .parse() 267 | .map_err(|err| Error::ParseTimestamp(err, line_number))?, 268 | 0, 269 | ) 270 | .ok_or(Error::TimeFinishedMissing)?; 271 | 272 | let result = code 273 | .parse() 274 | .map_err(|err| Error::ParseResultCode(err, line_number))?; 275 | 276 | if command.ends_with('\\') { 277 | acc_time_finished = Some(time_finished); 278 | acc_result = Some(result); 279 | acc_command = Some(format!("{}\n", command.trim_end_matches('\\'))); 280 | multiline_command = true; 281 | } else { 282 | entries.push(HistfileEntry { 283 | time_finished, 284 | result, 285 | command, 286 | }); 287 | } 288 | } else if let Some(ref mut acc) = acc_command { 289 | acc.push_str(&line); 290 | acc.push('\n'); 291 | } else { 292 | unreachable!("line not starting with : and no multiline command"); 293 | } 294 | } 295 | 296 | if let Some(command) = acc_command { 297 | let time_finished = acc_time_finished.expect("shoudnt fail if command is some"); 298 | let result = acc_result.expect("shoudnt fail if command is some"); 299 | 300 | entries.push(HistfileEntry { 301 | time_finished, 302 | result, 303 | command, 304 | }); 305 | } 306 | 307 | let store = crate::store::new(data_dir); 308 | 309 | let hostname = hostname::get() 310 | .map_err(Error::GetHostname)? 311 | .to_string_lossy() 312 | .to_string(); 313 | 314 | let base_dirs = directories::BaseDirs::new().ok_or(Error::BaseDirectory)?; 315 | let pwd = base_dirs.home_dir().to_path_buf(); 316 | let user = std::env::var("USER").map_err(Error::GetUser)?; 317 | let session_id = Uuid::new_v4(); 318 | 319 | for histfile_entry in entries { 320 | let time_finished = histfile_entry.time_finished; 321 | let time_start = histfile_entry.time_finished; 322 | let result = histfile_entry.result; 323 | let command = histfile_entry.command; 324 | let hostname = hostname.clone(); 325 | let pwd = pwd.clone(); 326 | let user = user.clone(); 327 | 328 | let entry = crate::entry::Entry { 329 | time_finished, 330 | time_start, 331 | hostname, 332 | command, 333 | pwd, 334 | result, 335 | session_id, 336 | user, 337 | }; 338 | 339 | store.add_entry(&entry)?; 340 | } 341 | 342 | Ok(()) 343 | } 344 | -------------------------------------------------------------------------------- /src/run/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod import; 2 | 3 | use crate::{ 4 | client, 5 | config, 6 | entry::Entry, 7 | message, 8 | message::{ 9 | CommandFinished, 10 | CommandStart, 11 | Message, 12 | session_id_from_env, 13 | }, 14 | server, 15 | store, 16 | store::{ 17 | Filter, 18 | filter, 19 | }, 20 | }; 21 | use chrono::{ 22 | DateTime, 23 | Local, 24 | Utc, 25 | }; 26 | use comfy_table::{ 27 | Attribute, 28 | Cell, 29 | Table, 30 | }; 31 | use log::{ 32 | debug, 33 | warn, 34 | }; 35 | use std::{ 36 | convert::TryInto, 37 | io::Write, 38 | path::{ 39 | Path, 40 | PathBuf, 41 | }, 42 | }; 43 | use thiserror::Error; 44 | use uuid::Uuid; 45 | 46 | #[derive(Error, Debug)] 47 | pub enum Error { 48 | #[error("{0}")] 49 | Client(#[from] client::Error), 50 | 51 | #[error("{0}")] 52 | Message(#[from] message::Error), 53 | 54 | #[error("{0}")] 55 | ServerBuilder(#[from] server::BuilderError), 56 | 57 | #[error("{0}")] 58 | Server(#[from] server::Error), 59 | 60 | #[error("{0}")] 61 | Store(#[from] store::Error), 62 | 63 | #[error("{0}")] 64 | Filter(#[from] filter::Error), 65 | 66 | #[error("can not get base directories")] 67 | GetBaseDirectories, 68 | 69 | #[error("can not convert chrono milliseconds: {0}")] 70 | ConvertDuration(std::num::TryFromIntError), 71 | 72 | #[error("can not write to stdout: {0}")] 73 | WriteStdout(std::io::Error), 74 | 75 | #[error("can not read configuration file: {0}")] 76 | ReadConfig(config::Error), 77 | 78 | #[error("encountered negative duration when trying to format duration")] 79 | NegativeDuration, 80 | 81 | #[cfg(feature = "histdb-import")] 82 | #[error("can not import from histdb: {0}")] 83 | ImportHistdb(import::Error), 84 | 85 | #[error("can not import from histfile: {0}")] 86 | ImportHistfile(import::Error), 87 | 88 | #[error("can not format entry: {0}\nentry: {1:?}")] 89 | FormatEntry(Box, Entry), 90 | } 91 | 92 | #[derive(Debug)] 93 | pub struct TableDisplay { 94 | pub format: bool, 95 | 96 | pub duration: Display, 97 | pub header: Display, 98 | pub host: Display, 99 | pub pwd: Display, 100 | pub session: Display, 101 | pub status: Display, 102 | } 103 | 104 | impl Default for TableDisplay { 105 | fn default() -> Self { 106 | Self { 107 | format: true, 108 | 109 | duration: Display::Hide, 110 | header: Display::Show, 111 | host: Display::Hide, 112 | pwd: Display::Hide, 113 | session: Display::Hide, 114 | status: Display::Hide, 115 | } 116 | } 117 | } 118 | 119 | #[derive(Debug)] 120 | pub enum Display { 121 | Hide, 122 | Show, 123 | } 124 | 125 | impl Default for Display { 126 | fn default() -> Self { 127 | Self::Hide 128 | } 129 | } 130 | 131 | impl Display { 132 | const fn is_show(&self) -> bool { 133 | match self { 134 | Self::Hide => false, 135 | Self::Show => true, 136 | } 137 | } 138 | 139 | pub const fn should_hide(b: bool) -> Self { 140 | if b { Self::Hide } else { Self::Show } 141 | } 142 | 143 | pub const fn should_show(b: bool) -> Self { 144 | if b { Self::Show } else { Self::Hide } 145 | } 146 | } 147 | 148 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 149 | pub fn default(filter: &Filter, display: &TableDisplay, data_dir: PathBuf) -> Result<(), Error> { 150 | let entries = store::new(data_dir).get_entries(filter)?; 151 | 152 | if display.format { 153 | default_format(display, entries); 154 | 155 | Ok(()) 156 | } else { 157 | default_no_format(display, entries) 158 | } 159 | } 160 | 161 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 162 | pub fn default_no_format(display: &TableDisplay, entries: Vec) -> Result<(), Error> { 163 | let mut header = vec!["tmn"]; 164 | 165 | if display.host.is_show() { 166 | header.push("host"); 167 | } 168 | 169 | if display.duration.is_show() { 170 | header.push("duration"); 171 | } 172 | 173 | if display.status.is_show() { 174 | header.push("res"); 175 | } 176 | 177 | if display.session.is_show() { 178 | header.push("ses"); 179 | } 180 | 181 | if display.pwd.is_show() { 182 | header.push("pwd"); 183 | } 184 | 185 | header.push("cmd"); 186 | 187 | let stdout = std::io::stdout(); 188 | let mut handle = stdout.lock(); 189 | 190 | if display.header.is_show() { 191 | handle 192 | .write_all(header.join("\t").as_bytes()) 193 | .map_err(Error::WriteStdout)?; 194 | 195 | handle.write_all(b"\n").map_err(Error::WriteStdout)?; 196 | } 197 | 198 | for entry in entries { 199 | if let Err(err) = default_no_format_entry(&mut handle, display, &entry) { 200 | warn!("{}", Error::FormatEntry(Box::new(err), entry)); 201 | } 202 | } 203 | 204 | Ok(()) 205 | } 206 | 207 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 208 | fn default_no_format_entry( 209 | handle: &mut T, 210 | display: &TableDisplay, 211 | entry: &Entry, 212 | ) -> Result<(), Error> 213 | where 214 | T: Write, 215 | { 216 | let mut row = vec![format_timestamp(entry.time_finished)]; 217 | 218 | if display.host.is_show() { 219 | row.push(entry.hostname.clone()); 220 | } 221 | 222 | if display.duration.is_show() { 223 | row.push(format_duration(entry.time_start, entry.time_finished)?); 224 | } 225 | 226 | if display.status.is_show() { 227 | row.push(format!("{}", entry.result)); 228 | } 229 | 230 | if display.session.is_show() { 231 | row.push(format_uuid(entry.session_id)); 232 | } 233 | 234 | if display.pwd.is_show() { 235 | row.push(format_pwd(&entry.pwd)?); 236 | } 237 | 238 | row.push(format_command(&entry.command, display.format)); 239 | 240 | handle 241 | .write_all(row.join("\t").as_bytes()) 242 | .map_err(Error::WriteStdout)?; 243 | 244 | handle.write_all(b"\n").map_err(Error::WriteStdout)?; 245 | 246 | Ok(()) 247 | } 248 | 249 | pub fn default_format(display: &TableDisplay, entries: Vec) { 250 | let mut table = Table::new(); 251 | table.load_preset(" "); 252 | table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); 253 | 254 | let mut header = vec![Cell::new("tmn").add_attribute(Attribute::Bold)]; 255 | 256 | if display.host.is_show() { 257 | header.push(Cell::new("host").add_attribute(Attribute::Bold)); 258 | } 259 | 260 | if display.duration.is_show() { 261 | header.push(Cell::new("duration").add_attribute(Attribute::Bold)); 262 | } 263 | 264 | if display.status.is_show() { 265 | header.push(Cell::new("res").add_attribute(Attribute::Bold)); 266 | } 267 | 268 | if display.session.is_show() { 269 | header.push(Cell::new("ses").add_attribute(Attribute::Bold)); 270 | } 271 | 272 | if display.pwd.is_show() { 273 | header.push(Cell::new("pwd").add_attribute(Attribute::Bold)); 274 | } 275 | 276 | header.push(Cell::new("cmd").add_attribute(Attribute::Bold)); 277 | 278 | if display.header.is_show() { 279 | table.set_header(header); 280 | } 281 | 282 | for entry in entries { 283 | if let Err(err) = default_format_entry(&mut table, display, &entry) { 284 | warn!("{}", Error::FormatEntry(Box::new(err), entry)); 285 | } 286 | } 287 | 288 | println!("{table}"); 289 | } 290 | 291 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 292 | fn default_format_entry( 293 | table: &mut Table, 294 | display: &TableDisplay, 295 | entry: &Entry, 296 | ) -> Result<(), Error> { 297 | let mut row = vec![format_timestamp(entry.time_finished)]; 298 | 299 | if display.host.is_show() { 300 | row.push(entry.hostname.clone()); 301 | } 302 | 303 | if display.duration.is_show() { 304 | row.push(format_duration(entry.time_start, entry.time_finished)?); 305 | } 306 | 307 | if display.status.is_show() { 308 | row.push(format!("{}", entry.result)); 309 | } 310 | 311 | if display.session.is_show() { 312 | row.push(format_uuid(entry.session_id)); 313 | } 314 | if display.pwd.is_show() { 315 | row.push(format_pwd(&entry.pwd)?); 316 | } 317 | 318 | row.push(format_command(&entry.command, display.format)); 319 | 320 | table.add_row(row); 321 | 322 | Ok(()) 323 | } 324 | 325 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 326 | pub fn zsh_add_history( 327 | config: &config::Config, 328 | command: String, 329 | socket_path: PathBuf, 330 | ) -> Result<(), Error> { 331 | if config.ignore_space && command.starts_with(' ') { 332 | debug!("not recording a command starting with a space"); 333 | } else { 334 | let data = CommandStart::from_env(config, command)?; 335 | client::new(socket_path).send(&Message::CommandStart(data))?; 336 | } 337 | 338 | Ok(()) 339 | } 340 | 341 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 342 | pub fn server(cache_dir: PathBuf, socket: PathBuf, data_dir: PathBuf) -> Result<(), Error> { 343 | server::builder(cache_dir, data_dir, socket, true) 344 | .build()? 345 | .run()?; 346 | 347 | Ok(()) 348 | } 349 | 350 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 351 | pub fn stop(socket_path: PathBuf) -> Result<(), Error> { 352 | client::new(socket_path).send(&Message::Stop)?; 353 | 354 | Ok(()) 355 | } 356 | 357 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 358 | pub fn disable(socket_path: PathBuf) -> Result<(), Error> { 359 | let session_id = session_id_from_env()?; 360 | client::new(socket_path).send(&Message::Disable(session_id))?; 361 | 362 | Ok(()) 363 | } 364 | 365 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 366 | pub fn enable(socket_path: PathBuf) -> Result<(), Error> { 367 | let session_id = session_id_from_env()?; 368 | client::new(socket_path).send(&Message::Enable(session_id))?; 369 | 370 | Ok(()) 371 | } 372 | 373 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 374 | pub fn precmd(socket_path: PathBuf) -> Result<(), Error> { 375 | let data = CommandFinished::from_env()?; 376 | 377 | client::new(socket_path).send(&Message::CommandFinished(data))?; 378 | 379 | Ok(()) 380 | } 381 | 382 | pub fn session_id() { 383 | println!("{}", Uuid::new_v4()); 384 | } 385 | 386 | pub fn init() { 387 | println!("{}", include_str!("../../resources/init.zsh")); 388 | } 389 | 390 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 391 | pub fn bench(socket_path: PathBuf) -> Result<(), Error> { 392 | let client = client::new(socket_path); 393 | 394 | let mut start = CommandStart { 395 | command: "test".to_string(), 396 | hostname: "test_hostname".to_string(), 397 | pwd: PathBuf::from("/tmp/test_pwd"), 398 | session_id: Uuid::new_v4(), 399 | time_stamp: Utc::now(), 400 | user: "test_user".to_string(), 401 | }; 402 | 403 | let mut finished = CommandFinished { 404 | session_id: start.session_id, 405 | time_stamp: Utc::now(), 406 | result: 0, 407 | }; 408 | 409 | loop { 410 | start.time_stamp = Utc::now(); 411 | let message = Message::CommandStart(start.clone()); 412 | 413 | client.send(&message).expect("ignore"); 414 | 415 | finished.time_stamp = Utc::now(); 416 | let message = Message::CommandFinished(finished.clone()); 417 | 418 | client.send(&message).expect("ignore"); 419 | } 420 | } 421 | 422 | fn format_timestamp(timestamp: DateTime) -> String { 423 | let today = Local::now().date_naive(); 424 | let local = timestamp.with_timezone(&chrono::offset::Local); 425 | let date = local.date_naive(); 426 | 427 | if date == today { 428 | local.format("%H:%M").to_string() 429 | } else { 430 | local.date_naive().format("%Y-%m-%d").to_string() 431 | } 432 | } 433 | 434 | fn format_uuid(uuid: uuid::Uuid) -> String { 435 | let chars = uuid.to_string().chars().collect::>(); 436 | 437 | vec![chars[0], chars[1], chars[2], chars[3]] 438 | .into_iter() 439 | .collect() 440 | } 441 | 442 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 443 | fn format_pwd(pwd: impl AsRef) -> Result { 444 | let base_dirs = directories::BaseDirs::new().ok_or(Error::GetBaseDirectories)?; 445 | let home = base_dirs.home_dir(); 446 | 447 | if pwd.as_ref().starts_with(home) { 448 | let mut without_home = PathBuf::from("~"); 449 | 450 | let pwd_components = pwd.as_ref().components().skip(3); 451 | 452 | pwd_components.for_each(|component| without_home.push(component)); 453 | 454 | Ok(without_home.to_string_lossy().to_string()) 455 | } else { 456 | Ok(pwd.as_ref().to_string_lossy().to_string()) 457 | } 458 | } 459 | 460 | #[expect(clippy::result_large_err, reason = "will fix this if needed")] 461 | fn format_duration( 462 | time_start: DateTime, 463 | time_finished: DateTime, 464 | ) -> Result { 465 | let duration = time_finished - time_start; 466 | let duration_ms = duration.num_milliseconds(); 467 | 468 | if duration_ms < 0 { 469 | return Err(Error::NegativeDuration); 470 | } 471 | 472 | let duration_std = 473 | std::time::Duration::from_millis(duration_ms.try_into().map_err(Error::ConvertDuration)?); 474 | 475 | Ok(humantime::format_duration(duration_std) 476 | .to_string() 477 | .replace(' ', "")) 478 | } 479 | 480 | fn format_command(command: &str, format: bool) -> String { 481 | if format { 482 | command.trim().to_string() 483 | } else { 484 | command.trim().replace('\n', "\\n") 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /src/server/builder.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | Server, 3 | db, 4 | }; 5 | use crate::store; 6 | use crossbeam_utils::sync::WaitGroup; 7 | use std::{ 8 | os::unix::net::UnixDatagram, 9 | path::PathBuf, 10 | sync::{ 11 | Arc, 12 | atomic::AtomicBool, 13 | }, 14 | }; 15 | use thiserror::Error; 16 | 17 | #[derive(Error, Debug)] 18 | pub enum Error { 19 | #[error("no parent directory for socket path")] 20 | NoSocketPathParent, 21 | 22 | #[error("can not create socket parent directory: {0}")] 23 | CreateSocketPathParent(std::io::Error), 24 | 25 | #[error("can not bind to socket: {0}")] 26 | BindSocket(std::io::Error), 27 | 28 | #[error("{0}")] 29 | Db(#[from] db::Error), 30 | } 31 | 32 | pub struct Builder { 33 | pub(super) cache_dir: PathBuf, 34 | pub(super) data_dir: PathBuf, 35 | pub(super) socket: PathBuf, 36 | pub(super) handle_ctrlc: bool, 37 | } 38 | 39 | impl Builder { 40 | pub fn build(self) -> Result { 41 | let db = db::new(self.cache_dir)?; 42 | 43 | let socket_path_parent = self.socket.parent().ok_or(Error::NoSocketPathParent)?; 44 | std::fs::create_dir_all(socket_path_parent).map_err(Error::CreateSocketPathParent)?; 45 | let socket = UnixDatagram::bind(&self.socket).map_err(Error::BindSocket)?; 46 | 47 | let store = store::new(self.data_dir); 48 | 49 | let stopping = Arc::new(AtomicBool::new(false)); 50 | let wait_group = WaitGroup::new(); 51 | 52 | let handle_ctrlc = self.handle_ctrlc; 53 | 54 | Ok(Server { 55 | db, 56 | socket, 57 | socket_path: self.socket, 58 | store, 59 | stopping, 60 | wait_group, 61 | handle_ctrlc, 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/server/db.rs: -------------------------------------------------------------------------------- 1 | use crate::message::CommandStart; 2 | use bincode::serde::{ 3 | BorrowCompat, 4 | Compat, 5 | }; 6 | use std::path::Path; 7 | use thiserror::Error; 8 | use uuid::Uuid; 9 | 10 | #[derive(Error, Debug)] 11 | pub enum Error { 12 | #[error("can not open entries database: {0}")] 13 | OpenEntriesDatabase(sled::Error), 14 | 15 | #[error("can not open disabled_sessions database: {0}")] 16 | OpenDisabledSessionsDatabase(sled::Error), 17 | 18 | #[error("can not serialize data: {0}")] 19 | SerializeData(bincode::error::EncodeError), 20 | 21 | #[error("can not deserialize entry: {0}")] 22 | DeserializeEntry(bincode::error::DecodeError), 23 | 24 | #[error("{0}")] 25 | Sled(#[from] sled::Error), 26 | 27 | #[error("entry does not exist in db")] 28 | EntryNotExist, 29 | } 30 | 31 | pub fn new(path: impl AsRef) -> Result { 32 | let entries = sled::open(path.as_ref().join("entries")).map_err(Error::OpenEntriesDatabase)?; 33 | let disabled_sessions = sled::open(path.as_ref().join("disabled_sessions")) 34 | .map_err(Error::OpenDisabledSessionsDatabase)?; 35 | 36 | Ok(Db { 37 | entries, 38 | disabled_sessions, 39 | }) 40 | } 41 | 42 | pub struct Db { 43 | entries: sled::Db, 44 | disabled_sessions: sled::Db, 45 | } 46 | 47 | impl Db { 48 | pub fn contains_entry(&self, uuid: &Uuid) -> Result { 49 | let key = Self::serialize(BorrowCompat(uuid))?; 50 | let contains = self.entries.contains_key(key)?; 51 | 52 | Ok(contains) 53 | } 54 | 55 | pub fn is_session_disabled(&self, uuid: &Uuid) -> Result { 56 | let key = Self::serialize(BorrowCompat(uuid))?; 57 | let contains = self.disabled_sessions.contains_key(key)?; 58 | 59 | Ok(contains) 60 | } 61 | 62 | pub fn add_entry(&self, entry: &CommandStart) -> Result<(), Error> { 63 | let key = Self::serialize(BorrowCompat(&entry.session_id))?; 64 | let value = Self::serialize(BorrowCompat(entry))?; 65 | 66 | self.entries.insert(key, value)?; 67 | 68 | Ok(()) 69 | } 70 | 71 | pub fn remove_entry(&self, uuid: &Uuid) -> Result { 72 | let key = Self::serialize(BorrowCompat(uuid))?; 73 | 74 | let data = self.entries.remove(key)?.ok_or(Error::EntryNotExist)?; 75 | 76 | let entry = Self::deserialize_entry(&data)?; 77 | 78 | Ok(entry) 79 | } 80 | 81 | pub fn disable_session(&self, uuid: &Uuid) -> Result<(), Error> { 82 | let key = Self::serialize(BorrowCompat(uuid))?; 83 | let value = Self::serialize(true)?; 84 | 85 | self.disabled_sessions.insert(key, value)?; 86 | 87 | self.remove_entry(uuid)?; 88 | 89 | Ok(()) 90 | } 91 | 92 | pub fn enable_session(&self, uuid: &Uuid) -> Result<(), Error> { 93 | let key = Self::serialize(BorrowCompat(uuid))?; 94 | 95 | self.disabled_sessions.remove(&key)?; 96 | 97 | Ok(()) 98 | } 99 | 100 | fn serialize(data: impl bincode::Encode) -> Result, Error> { 101 | let bytes = bincode::encode_to_vec(&data, bincode::config::standard()) 102 | .map_err(Error::SerializeData)?; 103 | 104 | Ok(bytes) 105 | } 106 | 107 | fn deserialize_entry(data: &sled::IVec) -> Result { 108 | let (entry, _): (Compat, _) = 109 | bincode::decode_from_slice(data, bincode::config::standard()) 110 | .map_err(Error::DeserializeEntry)?; 111 | 112 | Ok(entry.0) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod builder; 2 | pub mod db; 3 | 4 | use bincode::serde::Compat; 5 | pub use builder::{ 6 | Builder, 7 | Error as BuilderError, 8 | }; 9 | 10 | use crate::{ 11 | client, 12 | entry::Entry, 13 | message::{ 14 | CommandFinished, 15 | CommandStart, 16 | Message, 17 | }, 18 | store::Store, 19 | }; 20 | use crossbeam_utils::sync::WaitGroup; 21 | use db::Db; 22 | use flume::{ 23 | Receiver, 24 | Sender, 25 | }; 26 | use log::{ 27 | info, 28 | warn, 29 | }; 30 | use std::{ 31 | os::unix::net::UnixDatagram, 32 | path::{ 33 | Path, 34 | PathBuf, 35 | }, 36 | sync::{ 37 | Arc, 38 | atomic::{ 39 | AtomicBool, 40 | Ordering, 41 | }, 42 | }, 43 | thread, 44 | }; 45 | use thiserror::Error; 46 | use uuid::Uuid; 47 | 48 | const BUFFER_SIZE: usize = 16_384; 49 | 50 | #[derive(Error, Debug)] 51 | pub enum Error { 52 | #[error("can not receive message from socket: {0}")] 53 | ReceiveFromSocket(std::io::Error), 54 | 55 | #[error("can not send received data to processing: {0}")] 56 | SendBuffer(flume::SendError>), 57 | 58 | #[error("can not deserialize message: {0}")] 59 | DeserializeMessage(bincode::error::DecodeError), 60 | 61 | #[error("can not receive data from channel: {0}")] 62 | ReceiveData(flume::RecvError), 63 | 64 | #[error("can not remove socket: {0}")] 65 | RemoveSocket(std::io::Error), 66 | 67 | #[error("can not setup ctrlc handler: {0}")] 68 | SetupCtrlHandler(ctrlc::Error), 69 | 70 | #[error("command for session already started")] 71 | SessionCommandAlreadyStarted, 72 | 73 | #[error("command for session not started yet")] 74 | SessionCommandNotStarted, 75 | 76 | #[error("can not check if key exists in db: {0}")] 77 | CheckContainsEntry(db::Error), 78 | 79 | #[error("can not check if session is disabled in db: {0}")] 80 | CheckDisabledSession(db::Error), 81 | 82 | #[error("not recording because session {0} is disabled")] 83 | DisabledSession(Uuid), 84 | 85 | #[error("can not add entry to db: {0}")] 86 | AddDbEntry(db::Error), 87 | 88 | #[error("can not remove entry from db: {0}")] 89 | RemoveDbEntry(db::Error), 90 | 91 | #[error("can not add to storeo: {0}")] 92 | AddStore(crate::store::Error), 93 | 94 | #[error("db error: {0}")] 95 | Db(#[from] db::Error), 96 | } 97 | 98 | pub struct Server { 99 | pub(super) db: Db, 100 | pub(super) socket: UnixDatagram, 101 | pub(super) socket_path: PathBuf, 102 | pub(super) store: Store, 103 | pub(super) stopping: Arc, 104 | pub(super) wait_group: WaitGroup, 105 | pub(super) handle_ctrlc: bool, 106 | } 107 | 108 | pub fn builder( 109 | cache_dir: PathBuf, 110 | data_dir: PathBuf, 111 | socket: PathBuf, 112 | handle_ctrlc: bool, 113 | ) -> Builder { 114 | Builder { 115 | cache_dir, 116 | data_dir, 117 | socket, 118 | handle_ctrlc, 119 | } 120 | } 121 | 122 | impl Server { 123 | pub fn run(self) -> Result<(), Error> { 124 | let data_sender = Self::start_processor( 125 | Arc::clone(&self.stopping), 126 | self.wait_group.clone(), 127 | self.db, 128 | self.store, 129 | self.socket_path.clone(), 130 | ); 131 | 132 | Self::start_receiver( 133 | Arc::clone(&self.stopping), 134 | self.wait_group.clone(), 135 | self.socket, 136 | data_sender, 137 | ); 138 | 139 | if self.handle_ctrlc { 140 | Self::ctrl_c_watcher(self.stopping, self.socket_path.clone())?; 141 | } 142 | 143 | info!("listening on {}", self.socket_path.display()); 144 | 145 | self.wait_group.wait(); 146 | 147 | std::fs::remove_file(&self.socket_path).map_err(Error::RemoveSocket)?; 148 | 149 | Ok(()) 150 | } 151 | 152 | fn ctrl_c_watcher(stopping: Arc, socket_path: PathBuf) -> Result<(), Error> { 153 | ctrlc::set_handler(move || { 154 | stopping.store(true, Ordering::SeqCst); 155 | 156 | let client = client::new(socket_path.clone()); 157 | if let Err(err) = client.send(&Message::Stop) { 158 | warn!("{err}"); 159 | } 160 | }) 161 | .map_err(Error::SetupCtrlHandler)?; 162 | 163 | Ok(()) 164 | } 165 | 166 | fn start_receiver( 167 | stopping: Arc, 168 | wait_group: WaitGroup, 169 | socket: UnixDatagram, 170 | data_sender: Sender>, 171 | ) { 172 | thread::spawn(move || { 173 | loop { 174 | if stopping.load(Ordering::SeqCst) { 175 | break; 176 | } 177 | 178 | if let Err(err) = Self::receive(&socket, &data_sender) { 179 | warn!("{err}"); 180 | } 181 | } 182 | 183 | drop(wait_group); 184 | }); 185 | } 186 | 187 | fn receive(socket: &UnixDatagram, data_sender: &Sender>) -> Result<(), Error> { 188 | let mut buffer = [0_u8; BUFFER_SIZE]; 189 | let (written, _) = socket 190 | .recv_from(&mut buffer) 191 | .map_err(Error::ReceiveFromSocket)?; 192 | 193 | data_sender 194 | .send(buffer[0..written].to_vec()) 195 | .map_err(Error::SendBuffer)?; 196 | 197 | Ok(()) 198 | } 199 | 200 | fn start_processor( 201 | stopping: Arc, 202 | wait_group: WaitGroup, 203 | db: Db, 204 | store: Store, 205 | socket_path: PathBuf, 206 | ) -> Sender> { 207 | let (data_sender, data_receiver) = flume::bounded(10_000); 208 | 209 | thread::spawn(move || { 210 | loop { 211 | if stopping.load(Ordering::SeqCst) { 212 | break; 213 | } 214 | 215 | if let Err(err) = 216 | Self::process(&stopping, &data_receiver, &db, &store, &socket_path) 217 | { 218 | warn!("{err}"); 219 | } 220 | } 221 | 222 | while !data_receiver.is_empty() { 223 | if let Err(err) = 224 | Self::process(&stopping, &data_receiver, &db, &store, &socket_path) 225 | { 226 | warn!("{err}"); 227 | } 228 | } 229 | 230 | drop(wait_group); 231 | }); 232 | 233 | data_sender 234 | } 235 | 236 | fn process( 237 | stopping: &Arc, 238 | data_receiver: &Receiver>, 239 | db: &Db, 240 | store: &Store, 241 | socket_path: impl AsRef, 242 | ) -> Result<(), Error> { 243 | let data = data_receiver.recv().map_err(Error::ReceiveData)?; 244 | let (message, _): (Compat, _) = 245 | bincode::decode_from_slice(&data, bincode::config::standard()) 246 | .map_err(Error::DeserializeMessage)?; 247 | 248 | match message.0 { 249 | Message::Stop => { 250 | stopping.store(true, Ordering::SeqCst); 251 | 252 | let client = client::new(socket_path.as_ref().to_path_buf()); 253 | if let Err(err) = client.send(&Message::Stop) { 254 | warn!("{err}"); 255 | } 256 | 257 | Ok(()) 258 | } 259 | Message::CommandStart(data) => Self::command_start(db, &data), 260 | Message::CommandFinished(data) => Self::command_finished(db, store, &data), 261 | Message::Disable(uuid) => Self::disable_session(db, &uuid), 262 | Message::Enable(uuid) => Self::enable_session(db, &uuid), 263 | } 264 | } 265 | 266 | fn command_start(db: &Db, data: &CommandStart) -> Result<(), Error> { 267 | if db 268 | .contains_entry(&data.session_id) 269 | .map_err(Error::CheckContainsEntry)? 270 | { 271 | return Err(Error::SessionCommandAlreadyStarted); 272 | } 273 | 274 | if db 275 | .is_session_disabled(&data.session_id) 276 | .map_err(Error::CheckDisabledSession)? 277 | { 278 | return Err(Error::DisabledSession(data.session_id)); 279 | } 280 | 281 | db.add_entry(data).map_err(Error::AddDbEntry)?; 282 | 283 | Ok(()) 284 | } 285 | 286 | fn command_finished(db: &Db, store: &Store, data: &CommandFinished) -> Result<(), Error> { 287 | if db 288 | .is_session_disabled(&data.session_id) 289 | .map_err(Error::CheckDisabledSession)? 290 | { 291 | return Err(Error::DisabledSession(data.session_id)); 292 | } 293 | 294 | if !db 295 | .contains_entry(&data.session_id) 296 | .map_err(Error::CheckContainsEntry)? 297 | { 298 | return Err(Error::SessionCommandNotStarted); 299 | } 300 | 301 | let start = db 302 | .remove_entry(&data.session_id) 303 | .map_err(Error::RemoveDbEntry)?; 304 | 305 | let entry = Entry::from_messages(start, data); 306 | 307 | store.add(&entry).map_err(Error::AddStore)?; 308 | 309 | Ok(()) 310 | } 311 | 312 | fn disable_session(db: &Db, uuid: &Uuid) -> Result<(), Error> { 313 | db.disable_session(uuid)?; 314 | 315 | Ok(()) 316 | } 317 | 318 | fn enable_session(db: &Db, uuid: &Uuid) -> Result<(), Error> { 319 | db.enable_session(uuid)?; 320 | 321 | Ok(()) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/store/filter.rs: -------------------------------------------------------------------------------- 1 | use crate::entry::Entry; 2 | use regex::Regex; 3 | use std::path::PathBuf; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum Error { 8 | #[error("can not get hostname: {0}")] 9 | GetHostname(std::io::Error), 10 | 11 | #[error("can not get current directory: {0}")] 12 | GetCurrentDir(std::io::Error), 13 | } 14 | 15 | #[derive(Debug, Default)] 16 | pub struct Filter { 17 | pub hostname: Option, 18 | pub directory: Option, 19 | pub command: Option, 20 | pub no_subdirs: bool, 21 | pub command_text: Option, 22 | pub command_text_excluded: Option, 23 | pub count: usize, 24 | pub session: Option, 25 | pub failed: bool, 26 | pub find_status: Option, 27 | } 28 | 29 | impl Filter { 30 | pub const fn get_hostname(&self) -> Option<&String> { 31 | self.hostname.as_ref() 32 | } 33 | 34 | pub fn hostname(self, hostname: Option, all_hosts: bool) -> Result { 35 | let current_hostname = hostname::get() 36 | .map_err(Error::GetHostname)? 37 | .to_string_lossy() 38 | .to_string(); 39 | 40 | let hostname = if all_hosts { 41 | None 42 | } else { 43 | Some(hostname.unwrap_or(current_hostname)) 44 | }; 45 | 46 | Ok(Self { hostname, ..self }) 47 | } 48 | 49 | pub fn directory( 50 | self, 51 | folder: Option, 52 | in_current: bool, 53 | no_subdirs: bool, 54 | ) -> Result { 55 | let directory = if in_current { 56 | Some(std::env::current_dir().map_err(Error::GetCurrentDir)?) 57 | } else { 58 | folder 59 | }; 60 | 61 | Ok(Self { 62 | directory, 63 | no_subdirs, 64 | ..self 65 | }) 66 | } 67 | 68 | pub fn count(self, count: usize) -> Self { 69 | Self { count, ..self } 70 | } 71 | 72 | pub fn command( 73 | self, 74 | command: Option, 75 | command_text: Option, 76 | command_text_excluded: Option, 77 | ) -> Self { 78 | Self { 79 | command, 80 | command_text, 81 | command_text_excluded, 82 | ..self 83 | } 84 | } 85 | 86 | pub fn filter_entries(&self, entries: Vec) -> Vec { 87 | let filtered: Vec = entries 88 | .into_iter() 89 | .filter(|entry| { 90 | self.command 91 | .as_ref() 92 | .is_none_or(|command| Self::filter_command(&entry.command, command)) 93 | }) 94 | .filter(|entry| { 95 | self.directory.as_ref().is_none_or(|dir| { 96 | if self.no_subdirs { 97 | entry.pwd == *dir 98 | } else { 99 | entry.pwd.as_path().starts_with(dir) 100 | } 101 | }) 102 | }) 103 | .filter(|entry| { 104 | self.command_text 105 | .as_ref() 106 | .is_none_or(|regex| regex.is_match(&entry.command)) 107 | }) 108 | .filter(|entry| { 109 | self.command_text_excluded 110 | .as_ref() 111 | .is_none_or(|regex| !regex.is_match(&entry.command)) 112 | }) 113 | .filter(|entry| { 114 | self.session 115 | .as_ref() 116 | .is_none_or(|regex| regex.is_match(&entry.session_id.to_string())) 117 | }) 118 | .filter(|entry| !self.failed || entry.result == 0) 119 | .filter(|entry| { 120 | self.find_status 121 | .and_then(|find_status| { 122 | if find_status == entry.result { 123 | None 124 | } else { 125 | Some(()) 126 | } 127 | }) 128 | .is_none() 129 | }) 130 | .collect(); 131 | 132 | if self.count > 0 { 133 | filtered.into_iter().rev().take(self.count).rev().collect() 134 | } else { 135 | filtered 136 | } 137 | } 138 | 139 | pub fn session(self, session: Option) -> Self { 140 | Self { session, ..self } 141 | } 142 | 143 | pub fn filter_failed(self, filter_failed: bool) -> Self { 144 | Self { 145 | failed: filter_failed, 146 | ..self 147 | } 148 | } 149 | 150 | fn filter_command(entry_command: &str, command: &str) -> bool { 151 | entry_command 152 | .split('|') 153 | .any(|pipe_command| pipe_command.split_whitespace().next() == Some(command)) 154 | } 155 | 156 | pub fn find_status(self, find_status: Option) -> Self { 157 | Self { 158 | find_status, 159 | ..self 160 | } 161 | } 162 | } 163 | 164 | #[cfg(test)] 165 | mod test { 166 | use super::Filter; 167 | 168 | #[test] 169 | fn filter_command() { 170 | let cases = vec![ 171 | ("tr -d ' '", true), 172 | ("echo 'tr'", false), 173 | ("echo 'test test' | tr -d ' '", true), 174 | ("echo 'test test' | echo tr -d ' '", false), 175 | ("echo 'test test' | tr -d ' ' | tr -d 't'", true), 176 | ("", false), 177 | ("tr", true), 178 | ]; 179 | let check_command = "tr"; 180 | 181 | for (entry_command, result) in cases { 182 | assert_eq!(Filter::filter_command(entry_command, check_command), result); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/store/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod filter; 2 | 3 | use crate::entry::Entry; 4 | pub use filter::Filter; 5 | use std::{ 6 | fs, 7 | path::{ 8 | Path, 9 | PathBuf, 10 | }, 11 | }; 12 | use thiserror::Error; 13 | 14 | #[derive(Error, Debug)] 15 | pub enum Error { 16 | #[error("can not create log folder: {0}")] 17 | CreateLogFolder(PathBuf, std::io::Error), 18 | 19 | #[error("can not open log file: {0}")] 20 | OpenLogFile(PathBuf, std::io::Error), 21 | 22 | #[error("can not serialize entry: {0}")] 23 | SerializeEntry(csv::Error), 24 | 25 | #[error("glob is not valid: {0}")] 26 | InvalidGlob(glob::PatternError), 27 | 28 | #[error("problem while iterating glob: {0}")] 29 | GlobIteration(glob::GlobError), 30 | 31 | #[error("can not read log file {0:?}: {1}")] 32 | ReadLogFile(PathBuf, csv::Error), 33 | 34 | #[error("{0}")] 35 | Filter(#[from] filter::Error), 36 | } 37 | 38 | #[derive(Debug)] 39 | pub struct Store { 40 | data_dir: PathBuf, 41 | } 42 | 43 | pub const fn new(data_dir: PathBuf) -> Store { 44 | Store { data_dir } 45 | } 46 | 47 | impl Store { 48 | pub fn add_entry(&self, entry: &Entry) -> Result<(), Error> { 49 | let hostname = &entry.hostname; 50 | 51 | let folder_path = self.data_dir.as_path(); 52 | // Can't use .with_extension here as it will not work properly with hostnames 53 | // that contain dots. See test::dot_filename_with_extension for an 54 | // example. 55 | let file_path = folder_path.join(format!("{hostname}.csv")); 56 | 57 | fs::create_dir_all(folder_path) 58 | .map_err(|err| Error::CreateLogFolder(folder_path.to_path_buf(), err))?; 59 | 60 | let mut builder = csv::WriterBuilder::new(); 61 | 62 | // We only want to write the header if the file does not exist yet so we can 63 | // just append new entries to the existing file without having multiple 64 | // headers. 65 | builder.has_headers(!file_path.exists()); 66 | 67 | let index_file = std::fs::OpenOptions::new() 68 | .append(true) 69 | .create(true) 70 | .open(&file_path) 71 | .map_err(|err| Error::OpenLogFile(file_path.clone(), err))?; 72 | 73 | let mut writer = builder.from_writer(index_file); 74 | 75 | writer.serialize(entry).map_err(Error::SerializeEntry)?; 76 | 77 | Ok(()) 78 | } 79 | 80 | pub fn add(&self, entry: &Entry) -> Result<(), Error> { 81 | if entry.command.is_empty() { 82 | return Ok(()); 83 | } 84 | 85 | self.add_entry(entry)?; 86 | 87 | Ok(()) 88 | } 89 | 90 | pub fn get_entries(&self, filter: &Filter) -> Result, Error> { 91 | let mut entries: Vec<_> = if let Some(hostname) = filter.get_hostname() { 92 | let index_path = self.data_dir.join(format!("{hostname}.csv")); 93 | 94 | Self::read_log_file(index_path)? 95 | } else { 96 | let glob_string = self.data_dir.join("*.csv"); 97 | 98 | let glob = glob::glob(&glob_string.to_string_lossy()).map_err(Error::InvalidGlob)?; 99 | 100 | let index_paths = glob 101 | .collect::, glob::GlobError>>() 102 | .map_err(Error::GlobIteration)?; 103 | 104 | index_paths 105 | .into_iter() 106 | .map(Self::read_log_file) 107 | .collect::>, Error>>()? 108 | .into_iter() 109 | .flatten() 110 | .collect() 111 | }; 112 | 113 | entries.sort(); 114 | 115 | let entries = filter.filter_entries(entries); 116 | 117 | Ok(entries) 118 | } 119 | 120 | fn read_log_file>(file_path: P) -> Result, Error> { 121 | let file = std::fs::File::open(&file_path) 122 | .map_err(|err| Error::OpenLogFile(file_path.as_ref().to_path_buf(), err))?; 123 | 124 | let reader = std::io::BufReader::new(file); 125 | 126 | Self::read_metadata(reader) 127 | .map_err(|err| Error::ReadLogFile(file_path.as_ref().to_path_buf(), err)) 128 | } 129 | 130 | fn read_metadata(reader: R) -> Result, csv::Error> { 131 | let mut csv_reader = csv::ReaderBuilder::new().from_reader(reader); 132 | 133 | csv_reader 134 | .deserialize() 135 | .collect::, csv::Error>>() 136 | } 137 | } 138 | 139 | #[cfg(test)] 140 | mod test { 141 | #[test] 142 | fn dot_filename_with_extension() { 143 | let folder_path = std::path::PathBuf::from("/tmp"); 144 | let hostname = "test.test.test"; 145 | let expected = std::path::PathBuf::from(format!("/tmp/{hostname}.csv")); 146 | 147 | let bad = folder_path.join(hostname).with_extension("csv"); 148 | let good = folder_path.join(format!("{hostname}.csv")); 149 | 150 | assert_ne!(bad, expected); 151 | assert_eq!(good, expected); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /tests/client_server_integration.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | use std::{ 3 | path::PathBuf, 4 | sync::{ 5 | Arc, 6 | Barrier, 7 | }, 8 | thread, 9 | }; 10 | 11 | use chrono::Utc; 12 | use hstdb::{ 13 | client::{ 14 | self, 15 | Client, 16 | }, 17 | entry::Entry, 18 | message::{ 19 | CommandFinished, 20 | CommandStart, 21 | Message, 22 | }, 23 | server, 24 | store::{ 25 | self, 26 | Filter, 27 | }, 28 | }; 29 | use uuid::Uuid; 30 | 31 | struct TestClient { 32 | client: Client, 33 | barrier_stop: Arc, 34 | cache_dir: PathBuf, 35 | data_dir: PathBuf, 36 | 37 | keep_datadir: bool, 38 | } 39 | 40 | impl Drop for TestClient { 41 | fn drop(&mut self) { 42 | self.barrier_stop.wait(); 43 | 44 | std::fs::remove_dir_all(&self.cache_dir).unwrap(); 45 | 46 | if !self.keep_datadir { 47 | std::fs::remove_dir_all(&self.data_dir).unwrap(); 48 | } 49 | } 50 | } 51 | 52 | fn create_client_and_server(keep_datadir: bool) -> TestClient { 53 | let cache_dir = tempfile::tempdir().unwrap().into_path(); 54 | let data_dir = tempfile::tempdir().unwrap().into_path(); 55 | let socket = tempfile::NamedTempFile::new() 56 | .unwrap() 57 | .into_temp_path() 58 | .to_path_buf(); 59 | 60 | let barrier_start = Arc::new(Barrier::new(2)); 61 | let barrier_stop = Arc::new(Barrier::new(2)); 62 | 63 | { 64 | let barrier_start = Arc::clone(&barrier_start); 65 | let barrier_stop = Arc::clone(&barrier_stop); 66 | 67 | let cache_dir = cache_dir.clone(); 68 | let data_dir = data_dir.clone(); 69 | let socket = socket.clone(); 70 | 71 | let server = server::builder(cache_dir, data_dir, socket, false) 72 | .build() 73 | .unwrap(); 74 | 75 | thread::spawn(move || { 76 | barrier_start.wait(); 77 | server.run().unwrap(); 78 | barrier_stop.wait(); 79 | }); 80 | } 81 | 82 | barrier_start.wait(); 83 | 84 | let client = client::new(socket); 85 | 86 | TestClient { 87 | client, 88 | barrier_stop, 89 | cache_dir, 90 | data_dir, 91 | keep_datadir, 92 | } 93 | } 94 | 95 | #[test] 96 | fn stop_server() { 97 | let client = create_client_and_server(false); 98 | client.client.send(&Message::Stop).unwrap(); 99 | } 100 | 101 | #[test] 102 | fn write_entry() { 103 | let client = create_client_and_server(true); 104 | 105 | let session_id = Uuid::new_v4(); 106 | 107 | let start_data = CommandStart { 108 | command: "Test".to_string(), 109 | pwd: PathBuf::from("/tmp"), 110 | session_id, 111 | time_stamp: Utc::now(), 112 | user: "testuser".to_string(), 113 | hostname: "testhostname".to_string(), 114 | }; 115 | 116 | let finish_data = CommandFinished { 117 | session_id, 118 | time_stamp: Utc::now(), 119 | result: 0, 120 | }; 121 | 122 | client 123 | .client 124 | .send(&Message::CommandStart(start_data.clone())) 125 | .unwrap(); 126 | 127 | client 128 | .client 129 | .send(&Message::CommandFinished(finish_data.clone())) 130 | .unwrap(); 131 | 132 | client.client.send(&Message::Stop).unwrap(); 133 | 134 | let data_dir = client.data_dir.clone(); 135 | drop(client); 136 | 137 | let mut entries = store::new(data_dir.clone()) 138 | .get_entries(&Filter::default()) 139 | .unwrap(); 140 | 141 | std::fs::remove_dir_all(data_dir).unwrap(); 142 | 143 | dbg!(&entries); 144 | 145 | assert_eq!(entries.len(), 1); 146 | 147 | let got = entries.remove(0); 148 | let expected = Entry { 149 | time_finished: finish_data.time_stamp, 150 | time_start: start_data.time_stamp, 151 | hostname: start_data.hostname, 152 | command: start_data.command, 153 | pwd: start_data.pwd, 154 | result: finish_data.result, 155 | session_id: start_data.session_id, 156 | user: start_data.user, 157 | }; 158 | 159 | assert_eq!(expected, got); 160 | } 161 | 162 | #[test] 163 | fn write_entry_whitespace() { 164 | let client = create_client_and_server(true); 165 | 166 | let session_id = Uuid::new_v4(); 167 | 168 | let start_data = CommandStart { 169 | command: r#"Test\nTest\nTest "#.to_string(), 170 | pwd: PathBuf::from("/tmp"), 171 | session_id, 172 | time_stamp: Utc::now(), 173 | user: "testuser".to_string(), 174 | hostname: "testhostname".to_string(), 175 | }; 176 | 177 | let finish_data = CommandFinished { 178 | session_id, 179 | time_stamp: Utc::now(), 180 | result: 0, 181 | }; 182 | 183 | client 184 | .client 185 | .send(&Message::CommandStart(start_data.clone())) 186 | .unwrap(); 187 | 188 | client 189 | .client 190 | .send(&Message::CommandFinished(finish_data.clone())) 191 | .unwrap(); 192 | 193 | client.client.send(&Message::Stop).unwrap(); 194 | 195 | let data_dir = client.data_dir.clone(); 196 | drop(client); 197 | 198 | let mut entries = store::new(data_dir.clone()) 199 | .get_entries(&Filter::default()) 200 | .unwrap(); 201 | 202 | std::fs::remove_dir_all(data_dir).unwrap(); 203 | 204 | dbg!(&entries); 205 | 206 | assert_eq!(entries.len(), 1); 207 | 208 | let got = entries.remove(0); 209 | let expected = Entry { 210 | time_finished: finish_data.time_stamp, 211 | time_start: start_data.time_stamp, 212 | hostname: start_data.hostname, 213 | command: r#"Test\nTest\nTest"#.to_string(), 214 | pwd: start_data.pwd, 215 | result: finish_data.result, 216 | session_id: start_data.session_id, 217 | user: start_data.user, 218 | }; 219 | 220 | assert_eq!(expected, got); 221 | } 222 | 223 | // TODO: Make a test for this probably needs a restructuring of how we 224 | // detect leading spaces in commands 225 | //#[test] 226 | // fn write_command_starting_spaces() { 227 | // let client = create_client_and_server(true); 228 | // 229 | // let session_id = Uuid::new_v4(); 230 | // 231 | // let start_data = CommandStart { 232 | // command: " Test".to_string(), 233 | // pwd: PathBuf::from("/tmp"), 234 | // session_id: session_id.clone(), 235 | // time_stamp: Utc::now(), 236 | // user: "testuser".to_string(), 237 | // hostname: "testhostname".to_string(), 238 | // }; 239 | // 240 | // let finish_data = CommandFinished { 241 | // session_id, 242 | // time_stamp: Utc::now(), 243 | // result: 0, 244 | // }; 245 | // 246 | // client 247 | // .client 248 | // .send(&Message::CommandStart(start_data.clone())) 249 | // .unwrap(); 250 | // 251 | // client 252 | // .client 253 | // .send(&Message::CommandFinished(finish_data.clone())) 254 | // .unwrap(); 255 | // 256 | // client.client.send(&Message::Stop).unwrap(); 257 | // 258 | // let data_dir = client.data_dir.clone(); 259 | // drop(client); 260 | // 261 | // let entries = store::new(data_dir.clone()) 262 | // .get_entries(&Filter::default()) 263 | // .unwrap(); 264 | // 265 | // std::fs::remove_dir_all(data_dir).unwrap(); 266 | // 267 | // dbg!(&entries); 268 | // 269 | // assert_eq!(entries.len(), 0); 270 | //} 271 | 272 | #[test] 273 | fn write_empty_command() { 274 | let client = create_client_and_server(true); 275 | 276 | let session_id = Uuid::new_v4(); 277 | 278 | let start_data = CommandStart { 279 | command: "".to_string(), 280 | pwd: PathBuf::from("/tmp"), 281 | session_id, 282 | time_stamp: Utc::now(), 283 | user: "testuser".to_string(), 284 | hostname: "testhostname".to_string(), 285 | }; 286 | 287 | let finish_data = CommandFinished { 288 | session_id, 289 | time_stamp: Utc::now(), 290 | result: 0, 291 | }; 292 | 293 | client 294 | .client 295 | .send(&Message::CommandStart(start_data.clone())) 296 | .unwrap(); 297 | 298 | client 299 | .client 300 | .send(&Message::CommandFinished(finish_data.clone())) 301 | .unwrap(); 302 | 303 | client.client.send(&Message::Stop).unwrap(); 304 | 305 | let data_dir = client.data_dir.clone(); 306 | drop(client); 307 | 308 | let entries = store::new(data_dir.clone()) 309 | .get_entries(&Filter::default()) 310 | .unwrap(); 311 | 312 | std::fs::remove_dir_all(data_dir).unwrap(); 313 | 314 | dbg!(&entries); 315 | 316 | assert_eq!(entries.len(), 0); 317 | } 318 | 319 | #[test] 320 | fn write_newline_command() { 321 | let client = create_client_and_server(true); 322 | 323 | let session_id = Uuid::new_v4(); 324 | 325 | let commands = vec![ 326 | "\n".to_string(), 327 | "\r\n".to_string(), 328 | "\n\n".to_string(), 329 | "\n\n\n".to_string(), 330 | r#"\n"#.to_string(), 331 | '\n'.to_string(), 332 | '\r'.to_string(), 333 | ]; 334 | 335 | for command in commands { 336 | let start_data = CommandStart { 337 | command, 338 | pwd: PathBuf::from("/tmp"), 339 | session_id, 340 | time_stamp: Utc::now(), 341 | user: "testuser".to_string(), 342 | hostname: "testhostname".to_string(), 343 | }; 344 | 345 | let finish_data = CommandFinished { 346 | session_id, 347 | time_stamp: Utc::now(), 348 | result: 0, 349 | }; 350 | 351 | client 352 | .client 353 | .send(&Message::CommandStart(start_data.clone())) 354 | .unwrap(); 355 | 356 | client 357 | .client 358 | .send(&Message::CommandFinished(finish_data.clone())) 359 | .unwrap(); 360 | } 361 | 362 | client.client.send(&Message::Stop).unwrap(); 363 | 364 | let data_dir = client.data_dir.clone(); 365 | drop(client); 366 | 367 | let entries = store::new(data_dir.clone()) 368 | .get_entries(&Filter::default()) 369 | .unwrap(); 370 | 371 | std::fs::remove_dir_all(data_dir).unwrap(); 372 | 373 | dbg!(&entries); 374 | 375 | assert_eq!(entries.len(), 0); 376 | } 377 | --------------------------------------------------------------------------------