diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml new file mode 100644 index 00000000..57bb10df --- /dev/null +++ b/.github/workflows/dev-build.yml @@ -0,0 +1,121 @@ +name: Dev +on: + workflow_dispatch: + inputs: + source: + description: "Source ref used to build bindings. Uses `github.ref`` by default." + required: false + sha: + description: "Source SHA used to build bindings. Uses `github.sha`` by default." + required: false + push: + branches: [dev/**, refactor/**] + +env: + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + CARGO_TERM_COLOR: always + CARGO_TERM_PROGRESS_WHEN: never + CARGO_INCREMENTAL: 1 + # logging: + RUST_LOG: trace + CARGO_PLAYDATE_LOG: trace + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - macos-latest + - ubuntu-latest + - windows-latest + defaults: + run: + shell: bash + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.source || github.ref || github.event.ref }} + + - name: Cache + uses: actions/cache@v4.0.2 + with: + path: | + target/ + ~/.cargo + key: ${{ runner.os }}-dev-build-${{ hashFiles('Cargo.lock') }} + + - name: Config + run: | + mkdir -p .cargo + cp -rf .github/config.toml .cargo/config.toml + + - name: Cache LLVM + id: cache-llvm + if: runner.os == 'Windows' + uses: actions/cache@v4.0.2 + with: + path: ${{ runner.temp }}/llvm + key: llvm-14.0 + + # See: + # https://github.com/rust-lang/rust-bindgen/issues/1797 + # https://rust-lang.github.io/rust-bindgen/requirements.html#windows + - name: Install LLVM + if: runner.os == 'Windows' + uses: KyleMayes/install-llvm-action@v1.9.0 + with: + version: "14.0" + directory: ${{ runner.temp }}/llvm + cached: ${{ steps.cache-llvm.outputs.cache-hit }} + env: true + + + - name: Install linux deps + if: runner.os == 'Linux' + run: | + sudo apt install pkg-config -y + sudo apt install libudev-dev -y + + + - name: pdtool with + continue-on-error: true + run: cargo build -p=playdate-tool --bin=pdtool + + - name: Upload + id: upload + uses: actions/upload-artifact@v4 + with: + name: pdtool-${{ runner.os }}-${{ runner.arch }}${{ ((runner.os == 'Windows') && '.exe') || ' ' }} + path: target/debug/pdtool${{ ((runner.os == 'Windows') && '.exe') || ' ' }} + if-no-files-found: warn + retention-days: 3 + overwrite: true + - name: Artifact + run: | + echo 'ID: ${{ steps.upload.outputs.artifact-id }}' + echo 'URL: ${{ steps.upload.outputs.artifact-url }}' + + - name: pdtool with tracing + continue-on-error: true + run: cargo build -p=playdate-tool --bin=pdtool --features=tracing + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: pdtool+tracing-${{ runner.os }}-${{ runner.arch }}${{ ((runner.os == 'Windows') && '.exe') || ' ' }} + path: target/debug/pdtool${{ ((runner.os == 'Windows') && '.exe') || ' ' }} + if-no-files-found: warn + retention-days: 3 + overwrite: true + - name: Artifact + run: | + echo 'ID: ${{ steps.upload.outputs.artifact-id }}' + echo 'URL: ${{ steps.upload.outputs.artifact-url }}' + + outputs: + artifact-id: ${{ steps.upload.outputs.artifact-id }} + artifact-url: ${{ steps.upload.outputs.artifact-url }} diff --git a/Cargo.lock b/Cargo.lock index 855fc729..549a5441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -33,9 +42,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -46,6 +55,16 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "annotate-snippets" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b665789884a7e8fb06c84b295e923b03ca51edbb7d08f91a6a50322ecbfe6" +dependencies = [ + "anstyle", + "unicode-width", +] + [[package]] name = "anstream" version = "0.6.13" @@ -96,15 +115,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "arc-swap" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3d0060af21e8d11a926981cc00c6c1541aa91dd64b9f881985c3da1094425f" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "arrayvec" @@ -112,11 +131,119 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "async-trait" +version = "0.1.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] [[package]] name = "base16ct" @@ -142,7 +269,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -155,7 +282,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.52", + "syn 2.0.57", "which 4.4.2", ] @@ -167,9 +294,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bitmaps" @@ -196,7 +323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", - "regex-automata 0.4.5", + "regex-automata 0.4.6", "serde", ] @@ -211,9 +338,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "byteorder" @@ -223,9 +350,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bytesize" @@ -256,10 +383,11 @@ dependencies = [ [[package]] name = "cargo" -version = "0.77.0" +version = "0.78.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a399e5bde59d144aa2c7ba643765e2f8c6c3c601daa2da03202caf66f2552b3" +checksum = "d6305e39d08315644d79a5ae09a0745dfb3a43b5b5e318e55dbda3f12031c5dc" dependencies = [ + "annotate-snippets", "anstream", "anstyle", "anyhow", @@ -271,6 +399,7 @@ dependencies = [ "cargo-credential-wincred", "cargo-platform", "cargo-util", + "cargo-util-schemas", "clap", "color-print", "crates-io", @@ -281,7 +410,7 @@ dependencies = [ "git2", "git2-curl", "gix", - "gix-features 0.35.0", + "gix-features", "glob", "hex", "hmac", @@ -290,7 +419,7 @@ dependencies = [ "humantime", "ignore", "im-rc", - "indexmap", + "indexmap 2.2.6", "itertools 0.12.1", "jobserver", "lazycell", @@ -301,7 +430,6 @@ dependencies = [ "os_info", "pasetors", "pathdiff", - "pulldown-cmark", "rand", "regex", "rusqlite", @@ -309,13 +437,11 @@ dependencies = [ "semver", "serde", "serde-untagged", - "serde-value", "serde_ignored", "serde_json", "sha1", "shell-escape", - "supports-hyperlinks", - "syn 2.0.52", + "supports-hyperlinks 2.1.0", "tar", "tempfile", "time", @@ -325,7 +451,6 @@ dependencies = [ "tracing-subscriber", "unicase", "unicode-width", - "unicode-xid", "url", "walkdir", "windows-sys 0.52.0", @@ -333,9 +458,9 @@ dependencies = [ [[package]] name = "cargo-credential" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec27ad011c37339b865c765fa28096cd63d5b25fab680c04d9e410cb586c327d" +checksum = "4e5c02daf38715e60a9f59155bc3154c3e0bf55ee7bf34ddc090e8818c8f75e3" dependencies = [ "anyhow", "libc", @@ -348,9 +473,9 @@ dependencies = [ [[package]] name = "cargo-credential-libsecret" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b0ff7a44dd0af0fcd8d09bb1a6d7f7652847cba10aad017a6ea0a25ba7f00f" +checksum = "62d3e1abe5f85f1bb475901441daf4fbce5e6415c1b1ce7f40be92fd8cf678d5" dependencies = [ "anyhow", "cargo-credential", @@ -359,9 +484,9 @@ dependencies = [ [[package]] name = "cargo-credential-macos-keychain" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b7cf89a47dc2c20ae3a7c94335e151be32c20f85cc2790defdb1f5dac818de5" +checksum = "361100e0a3f7e5a3f3745bcda27637fce0e81dfa1deb00098085cbdacf5441d3" dependencies = [ "cargo-credential", "security-framework", @@ -369,9 +494,9 @@ dependencies = [ [[package]] name = "cargo-credential-wincred" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "341df45dc893bdffa36e2f7cbe3da90b38c5c74e7f4c0088ac801fd055b6df5b" +checksum = "4888956ebe36160e341031845a2db838d1e1be24ac6c20b2a02a8f593ff3692d" dependencies = [ "cargo-credential", "windows-sys 0.52.0", @@ -379,9 +504,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" dependencies = [ "serde", ] @@ -396,16 +521,19 @@ dependencies = [ "cargo", "cargo-platform", "cargo-util", + "cargo-util-schemas", "clap", "clap_lex", "dirs", "env_logger", "fs_extra", + "futures-lite", "log", - "nix", + "nix 0.28.0", "once_cell", "playdate-build", - "playdate-tool", + "playdate-device", + "playdate-simulator-utils", "rand", "regex", "semver", @@ -413,7 +541,7 @@ dependencies = [ "serde_json", "target", "toml", - "toml_edit 0.22.6", + "toml_edit 0.22.9", "try-lazy-init", "walkdir", "zip", @@ -421,9 +549,9 @@ dependencies = [ [[package]] name = "cargo-util" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74862c3c6e53a1c1f8f0178f9d38ab41e49746cd3a7cafc239b3d0248fd4e342" +checksum = "9f2d9a9a8d3e0b61b1110c49ab8f6ed7a76ce4f2b1d53ae48a83152d3d5e8f5b" dependencies = [ "anyhow", "core-foundation", @@ -442,12 +570,29 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "cargo-util-schemas" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63d2780ac94487eb9f1fea7b0d56300abc9eb488800854ca217f102f5caccca" +dependencies = [ + "semver", + "serde", + "serde-untagged", + "serde-value", + "thiserror", + "toml", + "unicode-xid", + "url", +] + [[package]] name = "cc" -version = "1.0.88" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ + "jobserver", "libc", ] @@ -501,9 +646,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -511,9 +656,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -526,14 +671,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -585,6 +730,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "console-api" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "const-hex" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba00838774b4ab0233e355d26710fbfc8327a05c017f6dc4873f876d1f79f78" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "proptest", + "serde", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -654,9 +849,9 @@ dependencies = [ [[package]] name = "crates-io" -version = "0.39.2" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6622f902c3c338eced1f000091f034846ae36aadaf35d0acd1ab0469a2d8ef1f" +checksum = "19958b4dfc8889cf78606e5e2fe64e7e0170a9ab853157192608f3a3253c8ef8" dependencies = [ "curl", "percent-encoding", @@ -822,6 +1017,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dunce" version = "1.0.4" @@ -899,9 +1100,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c012a26a7f605efc424dd53697843a72be7dc86ad2d01f7814337794a12231d" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ "anstream", "anstyle", @@ -918,9 +1119,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388979d208a049ffdfb22fa33b9c81942215b940910bccfe258caeb25d125cb3" +checksum = "2b73807008a3c7f171cc40312f37d95ef0396e048b5848d775f54b1a4dd4a0d3" dependencies = [ "serde", ] @@ -958,15 +1159,12 @@ name = "faster-hex" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" -dependencies = [ - "serde", -] [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "ff" @@ -980,9 +1178,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" +checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" [[package]] name = "filetime" @@ -992,7 +1190,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -1007,6 +1205,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1032,6 +1236,108 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1056,13 +1362,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "git2" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3ba52851e73b46a4c3df1d89343741112003f0f6f13beb0dfac9e457c3fdcd" +checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "libc", "libgit2-sys", "log", @@ -1085,9 +1397,9 @@ dependencies = [ [[package]] name = "gix" -version = "0.56.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0dcdc9c60d66535897fa40a7ea2a635e72f99456b1d9ae86b7e170e80618cb" +checksum = "6dd025382892c7b500a9ce1582cd803f9c2ebfe44aff52e9c7f86feee7ced75e" dependencies = [ "gix-actor", "gix-attributes", @@ -1098,7 +1410,7 @@ dependencies = [ "gix-date", "gix-diff", "gix-discover", - "gix-features 0.36.1", + "gix-features", "gix-filter", "gix-fs", "gix-glob", @@ -1131,7 +1443,7 @@ dependencies = [ "gix-validate", "gix-worktree", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "prodash", "smallvec", "thiserror", @@ -1140,9 +1452,9 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadca029ef716b4378f7afb19f7ee101fde9e58ba1f1445971315ac866db417" +checksum = "da27b5ab4ab5c75ff891dccd48409f8cc53c28a79480f1efdd33184b2dc1d958" dependencies = [ "bstr", "btoi", @@ -1154,9 +1466,9 @@ dependencies = [ [[package]] name = "gix-attributes" -version = "0.20.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f395469d38c76ec47cd1a6c5a53fbc3f13f737b96eaf7535f4e6b367e643381" +checksum = "bd6de7603d6bcefcf9a1d87779c4812b14665f71bc870df7ce9ca4c4b309de18" dependencies = [ "bstr", "gix-glob", @@ -1171,27 +1483,27 @@ dependencies = [ [[package]] name = "gix-bitmap" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b6cd0f246180034ddafac9b00a112f19178135b21eb031b3f79355891f7325" +checksum = "a371db66cbd4e13f0ed9dc4c0fea712d7276805fccc877f77e96374d317e87ae" dependencies = [ "thiserror", ] [[package]] name = "gix-chunk" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003ec6deacf68076a0c157271a127e0bb2c031c1a41f7168cbe5d248d9b85c78" +checksum = "45c8751169961ba7640b513c3b24af61aa962c967aaf04116734975cd5af0c52" dependencies = [ "thiserror", ] [[package]] name = "gix-command" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbea18732d602afc6daa73657c07d8cc0bcb3ff50872de1be5c8706954137fc" +checksum = "f90009020dc4b3de47beed28e1334706e0a330ddd17f5cfeb097df3b15a54b77" dependencies = [ "bstr", "gix-path", @@ -1201,13 +1513,13 @@ dependencies = [ [[package]] name = "gix-commitgraph" -version = "0.22.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a7007ba021f059803afaf6f8a48872422abc20550ac12ede6ddea2936cec36" +checksum = "7e8dcbf434951fa477063e05fea59722615af70dc2567377e58c2f7853b010fc" dependencies = [ "bstr", "gix-chunk", - "gix-features 0.36.1", + "gix-features", "gix-hash", "memmap2", "thiserror", @@ -1215,13 +1527,13 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.32.1" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0341471d55d8676e98b88e121d7065dfa4c9c5acea4b6d6ecdd2846e85cce0c3" +checksum = "367304855b369cadcac4ee5fb5a3a20da9378dd7905106141070b79f85241079" dependencies = [ "bstr", "gix-config-value", - "gix-features 0.36.1", + "gix-features", "gix-glob", "gix-path", "gix-ref", @@ -1236,11 +1548,11 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ab5d22bc21840f4be0ba2e78df947ba14d8ba6999ea798f86b5bdb999edd0c" +checksum = "fbd06203b1a9b33a78c88252a625031b094d9e1b647260070c25b09910c0a804" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "bstr", "gix-path", "libc", @@ -1249,9 +1561,9 @@ dependencies = [ [[package]] name = "gix-credentials" -version = "0.22.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513dac42450b27946bd0a0535a3a5a88e473d6522e5e3439a129cab779c88f3d" +checksum = "380cf3a7c31763743ae6403ec473281d54bfa05628331d09518a350ad5a0971f" dependencies = [ "bstr", "gix-command", @@ -1266,9 +1578,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17077f0870ac12b55d2eed9cb3f56549e40def514c8a783a0a79177a8a76b7c5" +checksum = "180b130a4a41870edfbd36ce4169c7090bca70e195da783dea088dd973daa59c" dependencies = [ "bstr", "itoa", @@ -1278,9 +1590,9 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.38.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8119a985887cfe68f4bdf92e51bd64bc758a73882d82fcfc03ebcb164441c85d" +checksum = "fd6a0454f8c42d686f17e7f084057c717c082b7dbb8209729e4e8f26749eb93a" dependencies = [ "bstr", "gix-hash", @@ -1290,9 +1602,9 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fad89416ebe0b3b7df78464124e2a02417b6cd3743d48ad93df86f4d2929c07" +checksum = "b8d7b2896edc3d899d28a646ccc6df729827a6600e546570b2783466404a42d6" dependencies = [ "bstr", "dunce", @@ -1305,30 +1617,19 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9ff423ae4983f762659040d13dd7a5defbd54b6a04ac3cc7347741cec828cd" -dependencies = [ - "crossbeam-channel", - "gix-hash", - "gix-trace", - "libc", - "parking_lot", -] - -[[package]] -name = "gix-features" -version = "0.36.1" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d46a4a5c6bb5bebec9c0d18b65ada20e6517dbd7cf855b87dd4bbdce3a771b2" +checksum = "d50270e8dcc665f30ba0735b17984b9535bdf1e646c76e638e007846164d57af" dependencies = [ "bytes", "crc32fast", + "crossbeam-channel", "flate2", "gix-hash", "gix-trace", "libc", "once_cell", + "parking_lot 0.12.1", "prodash", "sha1_smol", "thiserror", @@ -1337,9 +1638,9 @@ dependencies = [ [[package]] name = "gix-filter" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6a5c9d8e55c364e7c226919c19c9a28be1392d6208b5008059fa94ff7e2bf0" +checksum = "f598c1d688bf9d57f428ed7ee70c3e786d6f0cc7ed1aeb3c982135af41f6e516" dependencies = [ "bstr", "encoding_rs", @@ -1358,30 +1659,30 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20e86eb040f5776a5ade092282e51cdcad398adb77d948b88d17583c2ae4e107" +checksum = "7555c23a005537434bbfcb8939694e18cad42602961d0de617f8477cc2adecdd" dependencies = [ - "gix-features 0.36.1", + "gix-features", ] [[package]] name = "gix-glob" -version = "0.14.1" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db19298c5eeea2961e5b3bf190767a2d1f09b8802aeb5f258e42276350aff19" +checksum = "ae6232f18b262770e343dcdd461c0011c9b9ae27f0c805e115012aa2b902c1b8" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "bstr", - "gix-features 0.36.1", + "gix-features", "gix-path", ] [[package]] name = "gix-hash" -version = "0.13.3" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8cf8c2266f63e582b7eb206799b63aa5fa68ee510ad349f637dfe2d0653de0" +checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e" dependencies = [ "faster-hex", "thiserror", @@ -1389,20 +1690,20 @@ dependencies = [ [[package]] name = "gix-hashtable" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb61880816d7ec4f0b20606b498147d480860ddd9133ba542628df2f548d3ca" +checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242" dependencies = [ "gix-hash", - "hashbrown", - "parking_lot", + "hashbrown 0.14.3", + "parking_lot 0.12.1", ] [[package]] name = "gix-ignore" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a215cc8cf21645bca131fcf6329d3ebd46299c47dbbe27df71bb1ca9e328b879" +checksum = "f356ce440c60aedb7e72f3447f352f9c5e64352135c8cf33e838f49760fd2643" dependencies = [ "bstr", "gix-glob", @@ -1412,16 +1713,16 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.27.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f308f5cd2992e96a274b0d1931e9a0e44fdcba87695ead3f6df30d8a697e9c" +checksum = "9e50e63df6c8d4137f7fb882f27643b3a9756c468a1a2cdbe1ce443010ca8778" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "bstr", "btoi", "filetime", "gix-bitmap", - "gix-features 0.36.1", + "gix-features", "gix-fs", "gix-hash", "gix-lock", @@ -1437,9 +1738,9 @@ dependencies = [ [[package]] name = "gix-lock" -version = "11.0.1" +version = "12.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e5c65e6a29830a435664891ced3f3c1af010f14900226019590ee0971a22f37" +checksum = "f40a439397f1e230b54cf85d52af87e5ea44cc1e7748379785d3f6d03d802b00" dependencies = [ "gix-tempfile", "gix-utils", @@ -1448,22 +1749,22 @@ dependencies = [ [[package]] name = "gix-macros" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75e7ab728059f595f6ddc1ad8771b8d6a231971ae493d9d5948ecad366ee8bb" +checksum = "1dff438f14e67e7713ab9332f5fd18c8f20eb7eb249494f6c2bf170522224032" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] name = "gix-negotiate" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "979f6accd9c051b3dd018b50adf29c0a2459edddf6105cc70b767976cd6f8014" +checksum = "e6820bb5e9e259f6ad052826037452ca023d4f248c5d710dce067d89685dd582" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "gix-commitgraph", "gix-date", "gix-hash", @@ -1475,15 +1776,15 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.39.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "febf79c5825720c1c63fe974c7bbe695d0cb54aabad73f45671c60ce0e501e33" +checksum = "0c89402e8faa41b49fde348665a8f38589e461036475af43b6b70615a6a313a2" dependencies = [ "bstr", "btoi", "gix-actor", "gix-date", - "gix-features 0.36.1", + "gix-features", "gix-hash", "gix-validate", "itoa", @@ -1494,48 +1795,48 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.55.0" +version = "0.56.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fae5f971540c99c6ecc8d4368ecc9d18a9dc8b9391025c68c4399747dc93bac" +checksum = "46ae6da873de41c6c2b73570e82c571b69df5154dcd8f46dfafc6687767c33b1" dependencies = [ "arc-swap", "gix-date", - "gix-features 0.36.1", + "gix-features", "gix-hash", "gix-object", "gix-pack", "gix-path", "gix-quote", - "parking_lot", + "parking_lot 0.12.1", "tempfile", "thiserror", ] [[package]] name = "gix-pack" -version = "0.45.0" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569491c92446fddf373456ff360aff9a9effd627b40a70f2d7914dcd75a3205" +checksum = "782b4d42790a14072d5c400deda9851f5765f50fe72bca6dece0da1cd6f05a9a" dependencies = [ "clru", "gix-chunk", - "gix-features 0.36.1", + "gix-features", "gix-hash", "gix-hashtable", "gix-object", "gix-path", "gix-tempfile", "memmap2", - "parking_lot", + "parking_lot 0.12.1", "smallvec", "thiserror", ] [[package]] name = "gix-packetline" -version = "0.17.3" +version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ff45eef7747bde4986429a3e813478d50c2688b8f239e57bd3aa81065b285f" +checksum = "b70486beda0903b6d5b65dfa6e40585098cdf4e6365ca2dff4f74c387354a515" dependencies = [ "bstr", "faster-hex", @@ -1557,9 +1858,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e0b521a5c345b7cd6a81e3e6f634407360a038c8b74ba14c621124304251b8" +checksum = "23623cf0f475691a6d943f898c4d0b89f5c1a2a64d0f92bce0e0322ee6528783" dependencies = [ "bstr", "gix-trace", @@ -1570,11 +1871,11 @@ dependencies = [ [[package]] name = "gix-pathspec" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dbbb92f75a38ef043c8bb830b339b38d0698d7f3746968b5fcbade7a880494d" +checksum = "0cdb0ee9517c04f89bcaf6366fe893a17154ecb02d88b5c8174f27f1091d1247" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "bstr", "gix-attributes", "gix-config-value", @@ -1585,28 +1886,28 @@ dependencies = [ [[package]] name = "gix-prompt" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c04122ecca9079c27f6cd256cacbec1326b9c947d46d66ff8fb0d00e931656a1" +checksum = "f5325eb17ce7b5e5d25dec5c2315d642a09d55b9888b3bf46b7d72e1621a55d8" dependencies = [ "gix-command", "gix-config-value", - "parking_lot", + "parking_lot 0.12.1", "rustix", "thiserror", ] [[package]] name = "gix-protocol" -version = "0.42.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95736ef407db0bd15a5bdea791fbfcf523b9f13b96c852c240cd86a9ee0ef817" +checksum = "eca52738435991105f3bbd7f3a3a42cdf84c9992a78b9b7b1de528b3c022cfdd" dependencies = [ "bstr", "btoi", "gix-credentials", "gix-date", - "gix-features 0.36.1", + "gix-features", "gix-hash", "gix-transport", "maybe-async", @@ -1616,9 +1917,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1b102957d975c6eb56c2b7ad9ac7f26d117299b910812b2e9bf086ec43496d" +checksum = "cbff4f9b9ea3fa7a25a70ee62f545143abef624ac6aa5884344e70c8b0a1d9ff" dependencies = [ "bstr", "gix-utils", @@ -1627,13 +1928,13 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.39.1" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2069adc212cf7f3317ef55f6444abd06c50f28479dbbac5a86acf3b05cbbfe" +checksum = "64d9bd1984638d8f3511a2fcbe84fcedb8a5b5d64df677353620572383f42649" dependencies = [ "gix-actor", "gix-date", - "gix-features 0.36.1", + "gix-features", "gix-fs", "gix-hash", "gix-lock", @@ -1648,9 +1949,9 @@ dependencies = [ [[package]] name = "gix-refspec" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d9d3b82e1ee78fc0dc1c37ea5ea76c2dbc73f407db155f0dfcea285e583bee" +checksum = "be219df5092c1735abb2a53eccdf775e945eea6986ee1b6e7a5896dccc0be704" dependencies = [ "bstr", "gix-hash", @@ -1662,9 +1963,9 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.24.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5dd51710ce5434bc315ea30394fab483c5377276494edd79222b321a5a9544" +checksum = "aa78e1df3633bc937d4db15f8dca2abdb1300ca971c0fabcf9fa97e38cf4cd9f" dependencies = [ "bstr", "gix-date", @@ -1678,9 +1979,9 @@ dependencies = [ [[package]] name = "gix-revwalk" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d4ed2493ca94a475fdf147138e1ef8bab3b6ebb56abf3d9bda1c05372ec1dd" +checksum = "702de5fe5c2bbdde80219f3a8b9723eb927466e7ecd187cfd1b45d986408e45f" dependencies = [ "gix-commitgraph", "gix-date", @@ -1693,11 +1994,11 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022592a0334bdf77c18c06e12a7c0eaff28845c37e73c51a3e37d56dd495fb35" +checksum = "fddc27984a643b20dd03e97790555804f98cf07404e0e552c0ad8133266a79a1" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "gix-path", "libc", "windows-sys 0.52.0", @@ -1705,9 +2006,9 @@ dependencies = [ [[package]] name = "gix-submodule" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a3d7f60a95bdcaeb8981663c99d1c9f4de42aab1169524c949e948989809f9" +checksum = "21d438409222de24dffcc9897f04a9f97903a19fe4835b598ab3bb9b6e0f5e35" dependencies = [ "bstr", "gix-config", @@ -1720,35 +2021,35 @@ dependencies = [ [[package]] name = "gix-tempfile" -version = "11.0.1" +version = "12.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388dd29114a86ec69b28d1e26d6d63a662300ecf61ab3f4cc578f7d7dc9e7e23" +checksum = "a8ef376d718b1f5f119b458e21b00fbf576bc9d4e26f8f383d29f5ffe3ba3eaa" dependencies = [ "gix-fs", "libc", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "tempfile", ] [[package]] name = "gix-trace" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b202d766a7fefc596e2cc6a89cda8ad8ad733aed82da635ac120691112a9b1" +checksum = "9b838b2db8f62c9447d483a4c28d251b67fee32741a82cb4d35e9eb4e9fdc5ab" [[package]] name = "gix-transport" -version = "0.39.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f731cfefc4d62468c6dd2053f5c6707828256a6d2f5488c1811e3f42c178b144" +checksum = "be01a22053e9395a409fcaeed879d94f4fcffeb4f46de7143275fbf5e5b39770" dependencies = [ "base64", "bstr", "curl", "gix-command", "gix-credentials", - "gix-features 0.36.1", + "gix-features", "gix-packetline", "gix-quote", "gix-sec", @@ -1758,9 +2059,9 @@ dependencies = [ [[package]] name = "gix-traverse" -version = "0.35.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2112088122a0206592c84fbd42020db63b2ccaed66a0293779f2e5fbf80474" +checksum = "65109e445ba7a409b48f34f570a4d7db72eade1dc1bcff81990a490e86c07161" dependencies = [ "gix-commitgraph", "gix-date", @@ -1774,12 +2075,12 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.25.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c427a1a11ccfa53a4a2da47d9442c2241deee63a154bc15cc14b8312fbc4005" +checksum = "8f0f17cceb7552a231d1fec690bc2740c346554e3be6f5d2c41dfa809594dc44" dependencies = [ "bstr", - "gix-features 0.36.1", + "gix-features", "gix-path", "home", "thiserror", @@ -1788,9 +2089,9 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60157a15b9f14b11af1c6817ad7a93b10b50b4e5136d98a127c46a37ff16eeb6" +checksum = "0066432d4c277f9877f091279a597ea5331f68ca410efc874f0bdfb1cd348f92" dependencies = [ "fastrand", "unicode-normalization", @@ -1798,9 +2099,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7cc36f496bd5d96cdca0f9289bb684480725d40db60f48194aa7723b883854" +checksum = "e39fc6e06044985eac19dd34d474909e517307582e462b2eb4c8fa51b6241545" dependencies = [ "bstr", "thiserror", @@ -1808,13 +2109,13 @@ dependencies = [ [[package]] name = "gix-worktree" -version = "0.28.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1d0ae01dee14abe8c8117d78d7518f9a507de2dc4522546fbf4c444e9860b4" +checksum = "53982f8abff0789a9599e644108a1914da61a4d0dede8e45037e744dcb008d52" dependencies = [ "bstr", "gix-attributes", - "gix-features 0.36.1", + "gix-features", "gix-fs", "gix-glob", "gix-hash", @@ -1839,8 +2140,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.5", - "regex-syntax 0.8.2", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", ] [[package]] @@ -1854,6 +2155,31 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.3" @@ -1870,7 +2196,20 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown", + "hashbrown 0.14.3", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64", + "byteorder", + "flate2", + "nom", + "num-traits", ] [[package]] @@ -1879,6 +2218,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1946,6 +2291,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-auth" version = "0.1.9" @@ -1955,12 +2311,71 @@ dependencies = [ "memchr", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "idna" version = "0.5.0" @@ -1981,7 +2396,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.5", + "regex-automata 0.4.6", "same-file", "walkdir", "winapi-util", @@ -2003,12 +2418,22 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", ] [[package]] @@ -2020,6 +2445,36 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "is-terminal" version = "0.4.12" @@ -2031,6 +2486,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "itertools" version = "0.11.0" @@ -2051,9 +2512,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jni" @@ -2086,9 +2547,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -2102,6 +2563,29 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "lazy-regex" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.57", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2114,6 +2598,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lfs-core" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aa9f1a56e1178a04270bdd6f28b3a9cc34bc5429a3139cd03e2eae2ecb455a" +dependencies = [ + "lazy-regex", + "libc", + "snafu", +] + [[package]] name = "libc" version = "0.2.153" @@ -2136,14 +2631,20 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", "windows-targets 0.52.4", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libnghttp2-sys" version = "0.1.9+1.58.0" @@ -2160,9 +2661,9 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "libc", - "redox_syscall", + "redox_syscall 0.4.1", ] [[package]] @@ -2191,22 +2692,30 @@ dependencies = [ ] [[package]] -name = "libusb1-sys" -version = "0.6.4" +name = "libudev" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" dependencies = [ - "cc", "libc", "pkg-config", - "vcpkg", ] [[package]] name = "libz-sys" -version = "1.1.15" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" dependencies = [ "cc", "libc", @@ -2216,12 +2725,9 @@ dependencies = [ [[package]] name = "line-wrap" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" -dependencies = [ - "safemem", -] +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" [[package]] name = "linux-raw-sys" @@ -2251,6 +2757,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.11.0" @@ -2286,6 +2801,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "maybe-async" version = "0.2.10" @@ -2294,14 +2815,14 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memmap2" @@ -2312,6 +2833,52 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks 3.0.0", + "supports-unicode", + "terminal_size", + "textwrap", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2327,6 +2894,31 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio-serial" +version = "5.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a4c60ca5c9c0e114b3bd66ff4aa5f9b2b175442be51ca6c4365d687a97a2ac" +dependencies = [ + "log", + "mio", + "nix 0.26.4", + "serialport", + "winapi", +] + [[package]] name = "miow" version = "0.6.0" @@ -2338,9 +2930,22 @@ dependencies = [ [[package]] name = "new_debug_unreachable" -version = "1.0.4" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] [[package]] name = "nix" @@ -2348,7 +2953,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if", "cfg_aliases", "libc", @@ -2396,6 +3001,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", ] [[package]] @@ -2407,6 +3023,41 @@ dependencies = [ "libc", ] +[[package]] +name = "nusb" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac267a828c32759883a6097809fc4c32d76925099725aae562717cb023b431c" +dependencies = [ + "atomic-waker", + "core-foundation", + "core-foundation-sys", + "io-kit-sys", + "log", + "once_cell", + "rustix", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "object-pool" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee9a3e7196d09ec86002b939f1576e8e446d58def8fd48fe578e2c72d5328d68" +dependencies = [ + "parking_lot 0.11.2", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -2432,9 +3083,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -2470,13 +3121,13 @@ dependencies = [ [[package]] name = "os_info" -version = "3.7.0" +version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" dependencies = [ "log", "serde", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -2485,6 +3136,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" + [[package]] name = "p384" version = "0.13.0" @@ -2497,6 +3154,23 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -2504,7 +3178,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -2515,7 +3203,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets 0.48.5", ] @@ -2661,11 +3349,37 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs8" @@ -2715,8 +3429,8 @@ dependencies = [ "proc-macro2", "quote", "semver", - "syn 2.0.52", - "which 6.0.0", + "syn 2.0.57", + "which 6.0.1", ] [[package]] @@ -2768,6 +3482,34 @@ dependencies = [ "playdate-system", ] +[[package]] +name = "playdate-device" +version = "0.1.0" +dependencies = [ + "clap", + "const-hex", + "futures", + "futures-lite", + "hex", + "lfs-core", + "log", + "miette", + "nusb", + "object-pool", + "plist", + "regex", + "serde", + "serde_json", + "serialport", + "thiserror", + "tokio", + "tokio-serial", + "tracing", + "udev", + "usb-ids", + "windows", +] + [[package]] name = "playdate-display" version = "0.3.4" @@ -2812,6 +3554,17 @@ dependencies = [ "playdate-system", ] +[[package]] +name = "playdate-simulator-utils" +version = "0.1.0" +dependencies = [ + "log", + "playdate-build-utils", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "playdate-sound" version = "0.3.0-alpha.1" @@ -2834,7 +3587,7 @@ dependencies = [ [[package]] name = "playdate-sys" -version = "0.3.2" +version = "0.3.3" dependencies = [ "arrayvec", "playdate-bindgen", @@ -2850,19 +3603,23 @@ dependencies = [ [[package]] name = "playdate-tool" -version = "0.1.4" +version = "0.2.0" dependencies = [ "clap", + "console-subscriber", "env_logger", + "futures", + "futures-lite", "log", - "playdate-build", - "plist", - "regex", - "rusb", + "miette", + "playdate-device", + "playdate-simulator-utils", "serde", "serde_json", "thiserror", - "usb-ids", + "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -2880,12 +3637,12 @@ dependencies = [ [[package]] name = "plist" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" dependencies = [ "base64", - "indexmap", + "indexmap 2.2.6", "line-wrap", "quick-xml", "serde", @@ -2921,12 +3678,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" dependencies = [ "proc-macro2", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -2940,31 +3697,68 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "prodash" -version = "26.2.2" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794b5bf8e2d19b53dcdcec3e4bba628e20f5b6062503ba89281fa7037dd7bbcf" +checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" dependencies = [ - "parking_lot", + "parking_lot 0.12.1", ] [[package]] -name = "pulldown-cmark" -version = "0.9.6" +name = "proptest" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ - "bitflags 2.4.2", - "memchr", - "unicase", + "bitflags 2.5.0", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.8.3", + "unarray", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", ] [[package]] @@ -3015,6 +3809,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "rand_xoshiro" version = "0.6.0" @@ -3024,6 +3827,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3046,14 +3858,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.5", - "regex-syntax 0.8.2", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", ] [[package]] @@ -3067,13 +3879,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -3084,9 +3896,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rfc6979" @@ -3098,23 +3910,13 @@ dependencies = [ "subtle", ] -[[package]] -name = "rusb" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45fff149b6033f25e825cbb7b2c625a11ee8e6dac09264d49beb125e39aa97bf" -dependencies = [ - "libc", - "libusb1-sys", -] - [[package]] name = "rusqlite" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3122,6 +3924,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3130,9 +3938,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustfix" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec10cbeb92a2e494ef354d66126882da8c0a244ad769e2a7193efc5de625175" +checksum = "81864b097046da5df3758fdc6e4822bbb70afa06317e8ca45ea1b51cb8c5e5a4" dependencies = [ "serde", "serde_json", @@ -3142,11 +3950,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -3154,16 +3962,16 @@ dependencies = [ ] [[package]] -name = "ryu" -version = "1.0.17" +name = "rustversion" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] -name = "safemem" -version = "0.3.3" +name = "ryu" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "same-file" @@ -3205,9 +4013,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -3218,9 +4026,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -3272,7 +4080,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -3286,9 +4094,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -3304,6 +4112,25 @@ dependencies = [ "serde", ] +[[package]] +name = "serialport" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5a15d0be940df84846264b09b51b10b931fb2f275becb80934e3568a016828" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2", + "nix 0.26.4", + "regex", + "scopeguard", + "unescaper", + "winapi", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3359,6 +4186,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -3385,11 +4221,48 @@ dependencies = [ "typenum", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] [[package]] name = "socket2" @@ -3425,7 +4298,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "phf_shared 0.10.0", "precomputed-hash", "serde", @@ -3455,6 +4328,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "supports-color" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f" +dependencies = [ + "is_ci", +] + [[package]] name = "supports-hyperlinks" version = "2.1.0" @@ -3464,6 +4346,18 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "supports-hyperlinks" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "symlink" version = "0.1.0" @@ -3483,15 +4377,21 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "tar" version = "0.4.40" @@ -3541,24 +4441,35 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -3619,16 +4530,95 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "tokio-serial" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa6e2e4cf0520a99c5f87d5abb24172b5bd220de57c3181baaaa5440540c64aa" +dependencies = [ + "cfg-if", + "futures", + "log", + "mio-serial", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" -version = "0.8.10" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.6", + "toml_edit 0.22.9", ] [[package]] @@ -3646,7 +4636,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -3655,17 +4645,76 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.6" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", "winnow 0.6.5", ] +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.40" @@ -3685,7 +4734,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -3733,12 +4782,45 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8da954976b3cfd8a4d73b3ebb573e4b0f4c92326e5c1366a1c7b11e1eb11745" +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "udev" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50051c6e22be28ee6f217d50014f3bc29e81c20dc66ff7ca0d5c5226e1dcc5a1" +dependencies = [ + "io-lifetimes", + "libc", + "libudev-sys", + "pkg-config", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unescaper" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adf6ad32eb5b3cadff915f7b770faaac8f7ff0476633aa29eb0d9584d889d34" +dependencies = [ + "thiserror", +] + [[package]] name = "unicase" version = "2.7.0" @@ -3766,6 +4848,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.23" @@ -3851,6 +4939,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3859,9 +4956,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3869,24 +4966,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3894,22 +4991,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wax" @@ -3940,15 +5037,14 @@ dependencies = [ [[package]] name = "which" -version = "6.0.0" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" +checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" dependencies = [ "either", "home", - "once_cell", "rustix", - "windows-sys 0.52.0", + "winsafe", ] [[package]] @@ -3982,6 +5078,35 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-result" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd19df78e5168dfb0aedc343d1d1b8d422ab2db6756d2dc3fef75035402a3f64" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4132,6 +5257,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "xml5ever" version = "0.17.0" @@ -4160,7 +5291,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -4210,9 +5341,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.10+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 4742e5ed..20976e8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,8 @@ sys = { version = "0.3", path = "api/sys", package = "playdate-sys", default-fea tool = { version = "0.1", path = "support/tool", package = "playdate-tool" } build = { version = "0.2", path = "support/build", package = "playdate-build", default-features = false } utils = { version = "0.1", path = "support/utils", package = "playdate-build-utils", default-features = false } +device = { version = "0.1", path = "support/device", package = "playdate-device" } +simulator = { version = "0.1", path = "support/sim-ctrl", package = "playdate-simulator-utils", default-features = false } bindgen = { version = "0.1", path = "support/bindgen", package = "playdate-bindgen", default-features = false } bindgen-cfg = { version = "0.1", path = "support/bindgen-cfg", package = "playdate-bindgen-cfg", default-features = false } @@ -40,7 +42,10 @@ semver = "1.0" regex = "1" log = "0.4" env_logger = "0.11" -clap = "4.4" +clap = "4.5" serde = "1.0" serde_json = "1.0" toml = "0.8" +futures-lite = "2.3" +thiserror = "1.0" +tokio = { version = "1.37", default-features = false } diff --git a/cargo/Cargo.toml b/cargo/Cargo.toml index 9076fb52..6bb84d48 100644 --- a/cargo/Cargo.toml +++ b/cargo/Cargo.toml @@ -31,9 +31,10 @@ clap_lex = "0.7" dirs.workspace = true fs_extra.workspace = true -cargo = "=0.77.0" -cargo-util = "=0.2.9" -cargo-platform = "0.1.7" +cargo = "0.78.0" +cargo-util = "0.2.10" +cargo-platform = "0.1.8" +cargo-util-schemas = "0.2.0" semver.workspace = true serde = { workspace = true, features = ["derive"] } @@ -51,14 +52,21 @@ anstyle = "1" env_logger.workspace = true log.workspace = true +futures-lite.workspace = true + + [dependencies.build] workspace = true default-features = false features = ["assets-report", "toml"] -[dependencies.tool] +[dependencies.device] +workspace = true +features = ["clap", "async", "tokio-serial", "tokio"] + +[dependencies.simulator] +# features = ["tokio"] workspace = true -features = ["clap", "cli"] [dependencies.clap] workspace = true @@ -89,4 +97,3 @@ nix = { version = "0.28", features = ["signal"] } [features] default = [] -usb = ["tool/usb"] diff --git a/cargo/src/build/plan/format.rs b/cargo/src/build/plan/format.rs index 1832555c..579e161d 100644 --- a/cargo/src/build/plan/format.rs +++ b/cargo/src/build/plan/format.rs @@ -73,7 +73,7 @@ pub enum CompileModeProxy { Bench, /// A target that will be documented with `rustdoc`. /// If `deps` is true, then it will also document all dependencies. - Doc { deps: bool }, + Doc { deps: bool, json: bool }, /// A target that will be tested with `rustdoc`. Doctest, /// An example or library that will be scraped for function calls by `rustdoc`. diff --git a/cargo/src/cli/mod.rs b/cargo/src/cli/mod.rs index d39c9572..13ee4924 100644 --- a/cargo/src/cli/mod.rs +++ b/cargo/src/cli/mod.rs @@ -20,7 +20,6 @@ use cargo::util::command_prelude::{ArgMatchesExt, CompileMode, ProfileChecking}; use cargo::util::Config as CargoConfig; use cargo::util::CargoResult; use clap_lex::SeekFrom; -use tool::cli::mount::Mount; use crate::config::Config; use crate::logger::LogErr; @@ -220,7 +219,7 @@ pub fn initialize_from(args: impl IntoIterator + AsRe log::debug!("extra args: {extra:?}"); } - let no_wait = matches.flag("no-wait"); + let no_read = matches.flag("no-read"); let mounting = matches!(cmd, Cmd::Run).then(|| Mount::from_arg_matches(&matches).ok()) .flatten(); @@ -289,7 +288,7 @@ pub fn initialize_from(args: impl IntoIterator + AsRe sdk_path, gcc_path, mounting, - no_wait, + no_read, zip, no_info_meta, prevent_unwinding, diff --git a/cargo/src/cli/opts.rs b/cargo/src/cli/opts.rs index afeae114..1952fd02 100644 --- a/cargo/src/cli/opts.rs +++ b/cargo/src/cli/opts.rs @@ -8,8 +8,7 @@ use clap::{Arg, ArgAction, value_parser, Args}; use clap::Command; use playdate::consts::{SDK_ENV_VAR, DEVICE_TARGET}; use playdate::toolchain::gcc::ARM_GCC_PATH_ENV_VAR; -use tool::cli::run::DeviceDestination; -use tool::cli::mount::Mount; +// use tool::cli::mount::Mount; use super::{Cmd, CMD_NAME, BIN_NAME}; @@ -48,7 +47,7 @@ pub fn special_args_for(cmd: &Cmd) -> Vec { Cmd::Run => { let mut args = mount(); args.append(&mut shorthands_for(cmd)); - args.push(flag_no_wait()); + args.push(flag_no_read()); args.push(flag_no_info_file()); args.push(flag_pdc_skip_unknown()); args.push(flag_pdc_skip_prebuild()); @@ -231,6 +230,12 @@ fn init_crate() -> Command { } +#[derive(clap::Parser, Debug, Clone, Default)] +pub struct Mount { + #[command(flatten)] + pub device: device::device::query::Query, +} + fn mount() -> Vec { let mount: Command = Mount::augment_args(Command::new("mount")).mut_arg("device", |arg| arg.long("device").num_args(0..=1)); @@ -240,11 +245,11 @@ fn mount() -> Vec { .collect() } -fn flag_no_wait() -> Arg { - DeviceDestination::augment_args(Command::new("dest")).get_arguments() - .find(|arg| arg.get_id().as_str() == "no-wait") - .expect("Arg no-wait") - .to_owned() +fn flag_no_read() -> Arg { + flag( + "no-read", + "Do not wait & read the device's output after execution.", + ) } diff --git a/cargo/src/config.rs b/cargo/src/config.rs index 303e5fdd..2c8ed199 100644 --- a/cargo/src/config.rs +++ b/cargo/src/config.rs @@ -7,7 +7,6 @@ use cargo::core::compiler::{CompileTarget, CompileKind, TargetInfo}; use cargo::ops::CompileOptions; use playdate::toolchain::gcc::{ArmToolchain, Gcc}; use playdate::toolchain::sdk::Sdk; -use tool::cli::mount::Mount; use try_lazy_init::Lazy; use cargo::util::{CargoResult, Rustc}; @@ -16,6 +15,7 @@ use crate::build::rustflags::Rustflags; use crate::cli::cmd::Cmd; use crate::cli::deps::Dependency; use crate::cli::ide::Ide; +use crate::cli::opts::Mount; use crate::utils::LazyBuildContext; @@ -39,7 +39,7 @@ pub struct Config<'cfg> { pub gcc_path: Option, pub mounting: Option, - pub no_wait: bool, + pub no_read: bool, pub zip: bool, pub no_info_meta: bool, @@ -83,7 +83,7 @@ impl<'cfg> Config<'cfg> { sdk_path: Option, gcc_path: Option, mounting: Option, - no_wait: bool, + no_read: bool, zip: bool, no_info_meta: bool, prevent_unwinding: bool, @@ -111,7 +111,7 @@ impl<'cfg> Config<'cfg> { sdk_path, gcc_path, mounting, - no_wait, + no_read, zip, no_info_meta, prevent_unwinding, diff --git a/cargo/src/main.rs b/cargo/src/main.rs index 304257b6..4d53b871 100644 --- a/cargo/src/main.rs +++ b/cargo/src/main.rs @@ -203,22 +203,22 @@ fn execute(config: &Config) -> CargoResult<()> { // run: { - use tool::cli::run::run; - use tool::cli::run::{Run, Destination, SimDestination, DeviceDestination}; - use tool::cli::install::Install; - - let destination = if ck.is_playdate() { - Destination::Device(DeviceDestination { install: Install { pdx: package.path.to_owned(), - mount: config.mounting - .clone() - .unwrap_or_default() }, - no_install: false, - no_wait: config.no_wait }) + use futures_lite::future::block_on; + use device::run::run as run_dev; + use simulator::run::run as run_sim; + + if ck.is_playdate() { + let query = config.mounting.clone().unwrap_or_default().device; + let pdx = package.path.to_owned(); + let no_install = false; + let no_read = config.no_read; + let force = false; + let fut = run_dev(query, pdx, no_install, no_read, force); + block_on(fut)?; } else { - Destination::Simulator(SimDestination { pdx: package.path.to_owned() }) + let fut = run_sim(&package.path, config.sdk_path.as_deref()); + block_on(fut)?; }; - - run(Run { destination })?; } std::process::exit(0) diff --git a/cargo/src/package/mod.rs b/cargo/src/package/mod.rs index 80332017..57de0f10 100644 --- a/cargo/src/package/mod.rs +++ b/cargo/src/package/mod.rs @@ -6,13 +6,13 @@ use std::process::Command; use anyhow::anyhow; use anyhow::bail; -use cargo::util_schemas::manifest::TomlDebugInfo; use cargo::CargoResult; use cargo::core::Package; use cargo::core::compiler::CompileKind; use cargo::core::compiler::CrateType; use cargo::core::profiles::DebugInfo; use cargo::core::profiles::Profiles; +use cargo_util_schemas::manifest::TomlDebugInfo; use clap_lex::OsStrExt; use playdate::io::soft_link_checked; use playdate::layout::Layout; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b925678b..5a2cb0cc 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,6 @@ [toolchain] -channel = "nightly-2023-11-10" +# channel = "nightly-2023-11-10" +channel = "nightly" profile = "minimal" targets = ["thumbv7em-none-eabihf"] components = [ diff --git a/support/bindgen/Cargo.toml b/support/bindgen/Cargo.toml index b74e383c..4769b6bc 100644 --- a/support/bindgen/Cargo.toml +++ b/support/bindgen/Cargo.toml @@ -32,7 +32,7 @@ utils.workspace = true bindgen-cfg = { workspace = true, features = ["clap"] } [dependencies.bindgen] -version = "=0.69.4" +version = "0.69.4" default-features = false [dependencies.clap] diff --git a/support/device/Cargo.toml b/support/device/Cargo.toml new file mode 100644 index 00000000..139b06a0 --- /dev/null +++ b/support/device/Cargo.toml @@ -0,0 +1,80 @@ +[package] +name = "playdate-device" +version = "0.1.0" +readme = "README.md" +description = "Cross-platform interface Playdate device, async & blocking." +keywords = ["playdate", "usb", "serial"] +categories = ["hardware-support"] +edition.workspace = true +license.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true + + +[dependencies] +object-pool = "0.5" + +regex.workspace = true +log.workspace = true +miette = "7.2" +thiserror.workspace = true + +nusb = "0.1" +usb-ids = { version = "1.2024.2" } +serialport = { version = "4.3", features = ["usbportinfo-interface"] } +tokio-serial = { version = "5.4", optional = true } + +tracing = { version = "0.1", optional = true } + +# mb. read mount-points more correctly: +# rustix = "0.38" + +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +hex = "0.4" + +[dependencies.tokio] +features = ["fs", "process", "time", "io-std"] +workspace = true +optional = true + +[dependencies.futures-lite] +version = "2.3" + +[dependencies.futures] +version = "0.3" +optional = true + + +[dependencies.clap] +features = ["std", "env", "derive", "help", "color"] +workspace = true +optional = true + + +[target.'cfg(target_os = "macos")'.dependencies] +plist = "1.6" +const-hex = "1.11" + +[target.'cfg(target_os = "linux")'.dependencies] +udev = "0.8" +lfs-core = "0.11" + +[target.'cfg(target_os = "windows")'.dependencies.windows] +version = "0.54.0" +features = [ + "Win32_Foundation", + "Win32_Storage", + "Win32_Storage_FileSystem", + "Win32_System", + "Win32_System_IO", + "Win32_System_Ioctl", + "Win32_Security", +] + + +[features] +default = ["async"] +async = ["futures", "tokio", "tokio-serial"] +tokio-serial = ["dep:tokio-serial", "tokio?/io-util", "tokio?/rt"] diff --git a/support/device/README.md b/support/device/README.md new file mode 100644 index 00000000..7a8e182c --- /dev/null +++ b/support/device/README.md @@ -0,0 +1,43 @@ +# [Playdate][playdate-website] device support library + +Cross-platform interface for Playdate device, async & blocking. + +Contains methods for: +- find connected devices, filter by mode, state, serial-number +- send commands +- read from devices +- mount as drive (mass storage usb) +- unmount +- install pdx (playdate package) +- run pdx (optionally with install before run) +- operate with multiple devices simultaneously + + +### Status + +This crate in active development and API can be changed in future versions, with minor version increment. + +Supported platforms: +- MacOs +- Linux +- Windows + + +## Prerequisites + +1. Rust __nightly__ toolchain +2. Linux only: + - `libudev`, follow [instructions for udev crate][udev-crate-deps]. + + + +[playdate-website]: https://play.date +[udev-crate-deps]: https://crates.io/crates/udev#Dependencies + + + + + +- - - + +This software is not sponsored or supported by Panic. diff --git a/support/device/src/device/command.rs b/support/device/src/device/command.rs new file mode 100644 index 00000000..329a9681 --- /dev/null +++ b/support/device/src/device/command.rs @@ -0,0 +1,254 @@ +use std::borrow::Cow; +use std::fmt::Display; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; + + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "clap", derive(clap::Subcommand))] +#[cfg_attr(feature = "clap", command(name = "COMMAND"))] +pub enum Command { + /// Run custom pdx. + Run { + /// On-device path to the PDX package, + /// e.g. `/Games/MyGame.pdx` or `/System/Settings.pdx` + path: String, + }, + + /// Run system built-in pdx, + #[cfg_attr(feature = "clap", command(name = "run-sys"))] + RunSystem { + /// System built-in application, + #[cfg_attr(feature = "clap", arg(value_name = "NAME"))] + path: SystemPath, + }, + + /// Reboot into data segment USB disk + Datadisk, + + /// Hibernate, semi-deep sleep mode. + #[cfg_attr(feature = "clap", command(visible_alias = "sleep"))] + Hibernate, + + /// Turn console echo on or off. + Echo { + #[cfg_attr(feature = "clap", arg(default_value_t = Switch::On))] + value: Switch, + }, + + /// Request the device serial number. + #[cfg_attr(feature = "clap", command(visible_alias = "sn"))] + SerialNumber, + + /// Request the device version info. + #[cfg_attr(feature = "clap", command(visible_alias = "V"))] + Version, + + /// Simulate a button press. + /// + /// +a/-a/a for down/up/both + #[cfg_attr(feature = "clap", command(visible_alias = "btn"))] + Button { + /// Button to press or release. + #[cfg_attr(feature = "clap", clap(subcommand))] + button: Button, + }, + + /// Send a message to a message handler in the current running program. + #[cfg_attr(feature = "clap", command(visible_alias = "msg"))] + Message { + /// Message to send. + message: String, + }, + + /// Send custom command. + #[cfg_attr(feature = "clap", command(visible_alias = "!"))] + Custom { + /// Command to send. + cmd: String, + }, +} + + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "clap", derive(clap::Parser))] +pub enum PdxPath { + System { path: SystemPath }, + User { path: PathBuf }, +} + + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +pub enum SystemPath { + /// Launcher application, Home. + /// + /// `/System/Launcher.pdx`. + Launcher, + /// Settings application. + /// + /// `/System/Settings.pdx`. + Settings, + /// Playdate Catalog application. + /// + /// `/System/Catalog.pdx`. + Catalog, +} + +impl SystemPath { + pub fn as_path(&self) -> &Path { + match self { + Self::Launcher => Path::new("/System/Launcher.pdx"), + Self::Settings => Path::new("/System/Settings.pdx"), + Self::Catalog => Path::new("/System/Catalog.pdx"), + } + } +} + + +impl Command { + pub fn as_str(&self) -> Cow<'_, str> { + match self { + Command::Run { path } => format!("run {path}").into(), + Command::RunSystem { path } => format!("run {}", path.as_path().display()).into(), + Command::Datadisk => "datadisk".into(), + Command::Hibernate => "hibernate".into(), + Command::Echo { value: Switch::On } => "echo on".into(), + Command::Echo { value: Switch::Off } => "echo off".into(), + Command::SerialNumber => "serialread".into(), + Command::Version => "version".into(), + Command::Button { button } => format!("btn {}", button.as_btn_str()).into(), + Command::Message { message } => format!("msg {message}").into(), + Command::Custom { cmd } => format!("{cmd}").into(), + } + } +} + + +impl Display for Command { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.as_str().fmt(f) } +} + +impl Command { + pub fn with_break(&self) -> String { + let cmd = self.as_str(); + let mut line = String::with_capacity(cmd.len() + 2); + line.push('\n'); // extra break to ensure that the command starts from new line. + line.push_str(&cmd); + line.push('\n'); + line + } + + pub fn with_break_to(&self, mut writer: W) -> std::io::Result<()> { writeln!(writer, "\n{self}\n") } +} + + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +#[cfg_attr(feature = "clap", clap(name = "BOOL"))] +pub enum Switch { + /// Turn echo on. + /// [aliases: true] + #[cfg_attr(feature = "clap", value(alias = "true"))] + On, + /// Turn echo off. + /// [aliases: false] + #[cfg_attr(feature = "clap", value(alias = "false"))] + Off, +} + +impl From for Switch { + fn from(value: bool) -> Self { if value { Switch::On } else { Switch::Off } } +} + +impl Into for Switch { + fn into(self) -> bool { + match self { + Switch::On => true, + Switch::Off => false, + } + } +} + +impl std::fmt::Display for Switch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let value = match self { + Self::On => "on", + Self::Off => "off", + }; + write!(f, "{value}") + } +} + + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "clap", derive(clap::Subcommand))] +#[cfg_attr(feature = "clap", command(name = "BTN"))] +pub enum Button { + A { + #[cfg_attr(feature = "clap", arg(required = false, default_value_t = ButtonAction::Both))] + action: ButtonAction, + }, + B { + #[cfg_attr(feature = "clap", arg(required = false, default_value_t = ButtonAction::Both))] + action: ButtonAction, + }, +} + +impl std::fmt::Display for Button { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Button::A { action } => write!(f, "{action}a"), + Button::B { action } => write!(f, "{action}b"), + } + } +} + +impl Button { + pub fn as_btn_str(&self) -> String { + match self { + Button::A { action } => format!("{}a", action.as_btn_prefix()), + Button::B { action } => format!("{}b", action.as_btn_prefix()), + } + } +} + + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +#[cfg_attr(feature = "clap", clap(name = "BTN"))] +pub enum ButtonAction { + #[cfg_attr(feature = "clap", value(alias = "-"))] + Down, + #[cfg_attr(feature = "clap", value(alias = "+"))] + Up, + #[cfg_attr(feature = "clap", value(alias = "+-"), value(alias = "±"))] + Both, +} + +impl Default for ButtonAction { + fn default() -> Self { Self::Both } +} + +impl std::fmt::Display for ButtonAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let value = match self { + Self::Down => "-", + Self::Up => "+", + Self::Both => "±", + }; + write!(f, "{value}") + } +} + +impl ButtonAction { + pub fn as_btn_prefix(&self) -> &'static str { + match self { + Self::Down => "+", + Self::Up => "-", + Self::Both => "", + } + } +} diff --git a/support/device/src/device/methods.rs b/support/device/src/device/methods.rs new file mode 100644 index 00000000..6226e005 --- /dev/null +++ b/support/device/src/device/methods.rs @@ -0,0 +1,63 @@ +#![cfg(feature = "tokio")] + + +use crate::retry::{IterTime, Retries}; +use crate::usb::mode::Mode; +use crate::error::Error; + +use super::Device; + + +type Result = std::result::Result; + + +pub async fn wait_mode_storage(dev: Device, retry: Retries) -> Result + where T: IterTime { + wait_mode_change(dev, Mode::Storage, retry).await +} + +pub async fn wait_mode_data(dev: Device, retry: Retries) -> Result + where T: IterTime { + wait_mode_change(dev, Mode::Data, retry).await +} + + +#[cfg_attr(feature = "tracing", tracing::instrument(skip(dev, retry), fields(dev = dev.info().serial_number())))] +pub async fn wait_mode_change(mut dev: Device, to: Mode, retry: Retries) -> Result { + let total = &retry.total; + let iter_ms = retry.iters.interval(total); + let retries_num = total.as_millis() / iter_ms.as_millis(); + debug!("retries: {retries_num} * {iter_ms:?} ≈ {total:?}."); + + let mut counter = retries_num; + let mut interval = tokio::time::interval(iter_ms); + + while { + counter -= 1; + counter + } != 0 + { + interval.tick().await; + + let mode = dev.mode_cached(); + trace!( + "{dev}: waiting mode {to}, current: {mode}, try: {}/{retries_num}", + retries_num - counter + ); + + if mode == to { + dev.info().serial_number().map(|s| trace!("{s} is in {to} mode.")); + return Ok(dev); + } + + if dev.refresh()? { + if dev.mode_cached() == to { + return Ok(dev); + } else { + trace!("refreshed to {mode} mode, waiting...") + } + } + } + + Err(Error::usb_timeout(dev)) +} diff --git a/support/device/src/device/mod.rs b/support/device/src/device/mod.rs new file mode 100644 index 00000000..a41b7b58 --- /dev/null +++ b/support/device/src/device/mod.rs @@ -0,0 +1,108 @@ +use std::hash::Hash; +pub use nusb::DeviceInfo; +use crate::usb::mode::DeviceMode; +use crate::usb::mode::Mode; + +pub mod serial; +pub mod query; +pub mod command; + +mod methods; +pub use methods::*; + + +/// USB device wrapper +pub struct Device { + // pub serial: Option, + pub(crate) info: DeviceInfo, + pub(crate) mode: Mode, + + /// Opened device handle + pub(crate) inner: Option, + + // /// Claimed bulk data interface + // pub(crate) bulk: Option, + + // /// Opened serial fallback interface + // pub(crate) serial: Option, + pub(crate) interface: Option, +} + +impl Eq for Device {} +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + self.info.serial_number().is_some() && self.info.serial_number() == other.info.serial_number() + } +} + +impl Hash for Device { + fn hash(&self, state: &mut H) { + let info = &self.info; + info.serial_number().hash(state); + info.bus_number().hash(state); + info.device_address().hash(state); + info.vendor_id().hash(state); + info.product_id().hash(state); + info.class().hash(state); + info.subclass().hash(state); + info.protocol().hash(state); + info.manufacturer_string().hash(state); + info.product_string().hash(state); + self.inner.is_some().hash(state); + self.interface.is_some().hash(state); + } +} + + +impl AsRef for Device { + fn as_ref(&self) -> &DeviceInfo { &self.info } +} + +impl std::fmt::Display for Device { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}({})", + self.info.serial_number().unwrap_or("unknown"), + self.info.mode() + ) + } +} + +impl std::fmt::Debug for Device { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Device") + .field("sn", &self.info.serial_number()) + .field("mode", &self.mode) + .field("open", &self.is_open()) + .field("interface", &self.interface) + .finish() + } +} + + +impl Device { + pub fn new(info: DeviceInfo) -> Self { + Self { mode: info.mode(), + info, + inner: None, + interface: None } + } + + pub fn info(&self) -> &DeviceInfo { &self.info } + pub fn into_info(self) -> DeviceInfo { self.info } + + + // USB + + /// Cached mode of this device + pub fn mode_cached(&self) -> Mode { self.mode } + pub fn is_open(&self) -> bool { self.inner.is_some() || self.is_ready() } + pub fn is_ready(&self) -> bool { + match self.interface.as_ref() { + Some(crate::interface::Interface::Usb(_)) => true, + Some(crate::interface::Interface::Serial(inner)) => inner.is_open(), + None => false, + } + } +} diff --git a/support/device/src/device/query.rs b/support/device/src/device/query.rs new file mode 100644 index 00000000..88797f84 --- /dev/null +++ b/support/device/src/device/query.rs @@ -0,0 +1,139 @@ +use std::path::Path; +use std::path::PathBuf; +use std::str::FromStr; + +use super::serial::SerialNumber; + + +pub const DEVICE_SERIAL_ENV: &str = "PLAYDATE_SERIAL_DEVICE"; + + +/// Device query. Contains 4 options: +/// - None: query all devices +/// - Serial: query by serial number +/// - Path: query by path/name of serial port +/// - Com: query by COM port number (windows only) +#[derive(Clone)] +#[cfg_attr(feature = "clap", derive(clap::Parser))] +#[cfg_attr(feature = "clap", command(author, version, about, long_about = None, name = "device"))] +pub struct Query { + /// Serial number of usb device or absolute path to socket. + /// Format: 'PDUN-XNNNNNN' + #[cfg_attr(unix, doc = "or '/dev/cu.usbmodemPDUN_XNNNNNN(N)'.")] + #[cfg_attr(windows, doc = "or 'COM{X}', where {X} is a number of port, e.g.: COM3.")] + #[cfg_attr(feature = "clap", arg(env = DEVICE_SERIAL_ENV, name = "device"))] + pub value: Option, +} + +impl Default for Query { + fn default() -> Self { + Self { value: std::env::var(DEVICE_SERIAL_ENV).map(|s| Value::from_str(&s).ok()) + .ok() + .flatten() } + } +} + +impl std::fmt::Display for Query { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.value { + Some(ref value) => value.fmt(f), + None => write!(f, "None"), + } + } +} + +impl std::fmt::Debug for Query { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.value.as_ref() { + Some(value) => f.debug_tuple("Query").field(&value.to_string()).finish(), + None => f.debug_tuple("Query").field(&None::<()>).finish(), + } + } +} + +impl std::fmt::Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Serial(sn) => write!(f, "sn:{sn}"), + Value::Path(path) => write!(f, "serial:{}", path.display()), + Value::Com(port) => write!(f, "serial:COM{port}"), + } + } +} + + +#[derive(Clone, Debug)] +pub enum Value { + /// Serial number of usb device. + Serial(SerialNumber), + /// Absolute path of serial/modem/telnet-socket. + /// + /// In case of unmounting or installing it also can by mount-point. + #[cfg_attr(not(unix), doc = "Use only on Unix.")] + Path(PathBuf), + /// COM port. + #[cfg_attr(not(windows), doc = "Use only on Windows.")] + Com(u16), +} + +type ParseError = ::Err; +impl FromStr for Value { + type Err = crate::error::Error; + + fn from_str(dev: &str) -> Result { + let name = dev.trim(); + if name.is_empty() { + return Err(ParseError::from(name).into()); + } + + #[cfg(windows)] + match name.strip_prefix("COM").map(|s| s.parse::()) { + Some(Ok(com)) => return Ok(Value::Com(com)), + Some(Err(err)) => { + use std::io::{Error, ErrorKind}; + + return Err(Error::new( + ErrorKind::InvalidInput, + format!("Invalid format, seems to COM port, but {err}."), + ).into()); + }, + None => { /* nothing there */ }, + } + + let serial = SerialNumber::try_from(name); + let path = Path::new(name); + let is_direct = path.is_absolute() && path.exists(); + + match serial { + Ok(serial) => { + if is_direct { + Ok(Value::Path(path.to_owned())) + } else { + Ok(Value::Serial(serial)) + } + }, + Err(err) => { + if is_direct { + Ok(Value::Path(path.to_owned())) + } else { + Err(err.into()) + } + }, + } + } +} + +impl<'s> TryFrom<&'s str> for Value { + type Error = crate::error::Error; + fn try_from(dev: &'s str) -> Result { Self::from_str(dev) } +} + +impl Value { + pub fn to_printable_string(&self) -> String { + match self { + Self::Serial(sn) => sn.to_string(), + Self::Path(p) => p.display().to_string(), + Self::Com(n) => format!("COM{n}"), + } + } +} diff --git a/support/tool/src/model/serial.rs b/support/device/src/device/serial.rs similarity index 68% rename from support/tool/src/model/serial.rs rename to support/device/src/device/serial.rs index ce161220..c6ae4d04 100644 --- a/support/tool/src/model/serial.rs +++ b/support/device/src/device/serial.rs @@ -9,12 +9,6 @@ use regex::Regex; #[derive(Clone)] pub struct SerialNumber(String); -const UNKNOWN: &str = "UNKNOWN"; - -impl SerialNumber { - pub(crate) fn unknown() -> Self { Self(UNKNOWN.to_string()) } -} - impl SerialNumber { pub fn contained_in>(s: S) -> Option { @@ -45,10 +39,11 @@ impl SerialNumber { impl FromStr for SerialNumber { type Err = DeviceSerialFormatError; fn from_str(s: &str) -> Result { - Self::contained_in(s).ok_or(DeviceSerialFormatError(s.to_string())) + Self::contained_in(s).ok_or_else(|| DeviceSerialFormatError::from(s)) } } + impl TryFrom for SerialNumber { type Error = ::Err; fn try_from(value: String) -> Result { Self::from_str(value.as_str()) } @@ -66,8 +61,13 @@ impl TryFrom<&Path> for SerialNumber { impl PartialEq for SerialNumber { - fn eq(&self, other: &Self) -> bool { - (self.0 != UNKNOWN && other.0 != UNKNOWN) && (self.0.contains(&other.0) || other.0.contains(&self.0)) + fn eq(&self, other: &Self) -> bool { self.0.contains(&other.0) || other.0.contains(&self.0) } +} + +impl> PartialEq for SerialNumber { + fn eq(&self, other: &T) -> bool { + let other = other.as_ref().to_uppercase(); + self.0.contains(&other) || other.contains(&self.0) } } @@ -82,24 +82,30 @@ impl std::fmt::Display for SerialNumber { } -#[derive(Debug)] -pub struct DeviceSerialFormatError(String); -impl std::error::Error for DeviceSerialFormatError {} +use std::backtrace::Backtrace; +use thiserror::Error; +use miette::Diagnostic; -impl std::fmt::Display for DeviceSerialFormatError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Invalid serial number format: {}, should be PDUN-XNNNNNN", - self.0 - ) + +#[derive(Error, Debug, Diagnostic)] +#[error("Invalid serial number: {value}, expected format: PDUN-XNNNNNN.")] +pub struct DeviceSerialFormatError { + pub value: String, + #[backtrace] + backtrace: Backtrace, +} + +impl DeviceSerialFormatError { + fn new(value: String) -> Self { + Self { value, + backtrace: Backtrace::capture() } } } impl From for DeviceSerialFormatError { - fn from(value: String) -> Self { Self(value) } + fn from(value: String) -> Self { Self::new(value) } } impl From<&str> for DeviceSerialFormatError { - fn from(value: &str) -> Self { Self(value.to_owned()) } + fn from(value: &str) -> Self { Self::new(value.to_owned()) } } diff --git a/support/device/src/error.rs b/support/device/src/error.rs new file mode 100644 index 00000000..eec4aeab --- /dev/null +++ b/support/device/src/error.rs @@ -0,0 +1,177 @@ +use std::backtrace::Backtrace; +use thiserror::Error; +use miette::Diagnostic; +use crate::usb::mode::Mode; + + +#[derive(Error, Debug, Diagnostic)] +pub enum Error { + #[error(transparent)] + #[diagnostic(code(my_lib::io_error))] + Io { + #[backtrace] + #[from] + source: std::io::Error, + }, + + #[error(transparent)] + #[diagnostic()] + Process { + #[backtrace] + #[from] + source: std::process::ExitStatusError, + }, + + #[error(transparent)] + #[diagnostic()] + Transfer { + #[backtrace] + #[from] + source: nusb::transfer::TransferError, + }, + + #[error(transparent)] + #[diagnostic()] + Borrow { + #[backtrace] + #[from] + source: std::cell::BorrowMutError, + }, + + #[error("Awaiting device timeout `{device}`.")] + #[diagnostic()] + DeviceTimeout { + #[backtrace] + backtrace: Backtrace, + device: crate::device::Device, + }, + + #[error("Awaiting {what} timeout.")] + Timeout { + #[backtrace] + backtrace: Backtrace, + what: String, + }, + + #[error(transparent)] + #[diagnostic()] + Utf { + #[backtrace] + #[from] + source: std::str::Utf8Error, + }, + + #[error(transparent)] + Json { + #[backtrace] + #[from] + source: serde_json::Error, + }, + + #[cfg(target_os = "macos")] + #[error(transparent)] + Plist { + #[backtrace] + #[from] + source: plist::Error, + }, + + #[cfg(target_os = "linux")] + #[error(transparent)] + Lfs { + #[backtrace] + #[from] + source: lfs_core::Error, + }, + + #[cfg(target_os = "windows")] + #[error(transparent)] + WinApi { + #[backtrace] + #[from] + source: windows::core::Error, + }, + + #[error("Chain of errors: {source}\n\t{others:#?}")] + #[diagnostic()] + Chain { + #[backtrace] + #[diagnostic(transparent)] + source: Box, + #[related] + #[diagnostic(transparent)] + others: Vec, + }, + + #[error(transparent)] + #[diagnostic(transparent)] + DeviceSerial { + #[backtrace] + #[from] + source: crate::device::serial::DeviceSerialFormatError, + }, + + #[error(transparent)] + #[diagnostic()] + SerialPort { + #[backtrace] + #[from] + source: serialport::Error, + }, + + #[diagnostic()] + #[error("Device not found.")] + /// Device discovery error. + NotFound(#[backtrace] Backtrace), + + #[diagnostic()] + #[error("Interface not ready.")] + /// Interface error. + NotReady(#[backtrace] Backtrace), + + #[error("Device in the wrong state `{0:?}`.")] + WrongState(Mode), + + #[error("Mount point not found for {0}.")] + MountNotFound(String), + // #[error("data store disconnected")] + // Disconnect(#[from] io::Error), + // #[error("the data for key `{0}` is not available")] + // Redaction(String), + // #[error("invalid header (expected {expected:?}, found {found:?})")] + // InvalidHeader { expected: String, found: String }, + // #[error("unknown error")] + // Unknown, +} + + +impl Error { + #[track_caller] + pub fn usb_timeout(device: crate::device::Device) -> Self { + Self::DeviceTimeout { device, + backtrace: Backtrace::capture() } + } + + #[track_caller] + pub fn timeout(what: S) -> Self { + Self::Timeout { what: what.to_string(), + backtrace: Backtrace::capture() } + } + + #[track_caller] + pub fn not_found() -> Self { Self::NotFound(Backtrace::capture()) } + #[track_caller] + pub fn not_ready() -> Self { Self::NotReady(Backtrace::capture()) } + + #[track_caller] + pub fn chain(err: A, others: I) -> Self + where I: IntoIterator, + A: Into, + B: Into { + Self::Chain { source: Box::new(err.into()), + others: others.into_iter().map(Into::into).collect() } + } +} + + +unsafe impl Sync for Error {} diff --git a/support/device/src/install.rs b/support/device/src/install.rs new file mode 100644 index 00000000..8f5df0e2 --- /dev/null +++ b/support/device/src/install.rs @@ -0,0 +1,191 @@ +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use futures::{FutureExt, Stream, StreamExt, TryFutureExt}; + +use crate::device::query::Query; +use crate::error::Error; +use crate::mount::MountedDevice; +use crate::mount; +use crate::retry::Retries; + + +type Result = std::result::Result; + + +/// On-device path with owned mounted device. +pub struct MountedDevicePath { + drive: MountedDevice, + path: String, +} + +/// On-device path with borrowed mounted device. +pub struct MountedDevicePathBorrowed<'dev> { + drive: &'dev MountedDevice, + path: String, +} + +impl<'dev> MountedDevicePathBorrowed<'dev> { + pub fn drive(&self) -> &MountedDevice { &self.drive } + + /// Local on-device path. + pub fn path_local(&self) -> &str { &self.path } + /// Absolute on-host path. + pub fn path_abs(&self) -> PathBuf { self.drive.handle.path().join(&self.path) } + + pub fn into_path(self) -> String { self.path } + pub fn into_parts(self) -> (&'dev MountedDevice, String) { (self.drive, self.path) } + + pub fn to_owned_replacing(self) -> impl FnOnce(MountedDevice) -> MountedDevicePath { + let (_, path) = self.into_parts(); + move |drive| MountedDevicePath { drive, path } + } +} + +impl MountedDevicePath { + pub fn drive(&self) -> &MountedDevice { &self.drive } + pub fn drive_mut(&mut self) -> &mut MountedDevice { &mut self.drive } + + /// Local on-device path. + pub fn path_local(&self) -> &str { &self.path } + /// Absolute on-host path. + pub fn path_abs(&self) -> PathBuf { self.drive.handle.path().join(&self.path) } + + pub fn into_path(self) -> String { self.path } + pub fn into_parts(self) -> (MountedDevice, String) { (self.drive, self.path) } +} + + +/// Install package on the device. +/// +/// `path` is a host filesystem path to pdx. +#[cfg_attr(feature = "tracing", tracing::instrument(skip(drive)))] +pub async fn install<'dev>(drive: &'dev MountedDevice, + path: &Path, + force: bool) + -> Result> { + #[cfg(feature = "tokio")] + use tokio::process::Command; + #[cfg(not(feature = "tokio"))] + use std::process::Command; + + + let retry = Retries::new(Duration::from_millis(500), Duration::from_secs(60)); + mount::wait_fs_available(drive, retry).await?; + validate_host_package(path).await?; + + trace!( + "Installing: {} -> {}", + path.display(), + drive.handle.path().display() + ); + + let games = drive.handle.path().join("Games"); + + let cp = || { + async { + if cfg!(unix) { + let mut cmd = Command::new("cp"); + + if force { + cmd.arg("-r"); + } + + cmd.arg(path); + cmd.arg(&games); + + #[cfg(feature = "tokio")] + cmd.status().await?.exit_ok()?; + #[cfg(not(feature = "tokio"))] + cmd.status()?.exit_ok()?; + } else if cfg!(windows) { + // xcopy c:\test c:\test2\test /S /E /H /I /Y + let mut cmd = Command::new("xcopy"); + cmd.arg(path); + cmd.arg(games.join(path.file_name().unwrap())); + + cmd.args(["/S", "/E", "/H", "/I"]); + if force { + cmd.arg("/Y"); + } + + #[cfg(feature = "tokio")] + cmd.status().await?.exit_ok()?; + #[cfg(not(feature = "tokio"))] + cmd.status()?.exit_ok()?; + } else { + unreachable!("Unsupported OS") + } + Ok::<_, Error>(()) + } + }; + + if !path.is_dir() { + #[cfg(feature = "tokio")] + { + tokio::fs::copy(path, games.join(path.file_name().unwrap())).map_ok(|bytes| trace!("copied {bytes}")) + .inspect_err(|err| error!("{err}")) + .or_else(|_| cp()) + .await?; + }; + #[cfg(not(feature = "tokio"))] + { + std::fs::copy(path, games.join(path.file_name().unwrap())).map(|bytes| trace!("copied {bytes}")) + .inspect_err(|err| error!("{err}")) + .or_else(|_| { + futures_lite::future::block_on(cp()) + })?; + } + } else { + cp().await?; + } + + // on-dev-path: + let path = format!("/Games/{}", path.file_name().unwrap().to_string_lossy()); + Ok(MountedDevicePathBorrowed { drive, path }) +} + + +/// 1. Mount if needed +/// 1. Wait for FS to become available +/// 1. Install package +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn mount_and_install(query: Query, + path: &Path, + force: bool) + -> Result> + '_> { + validate_host_package(path).await?; + + // TODO: Check query is path and this is mounted volume. + + let fut = mount::mount_and(query, true).await?.flat_map(move |res| { + async move { + match res { + Ok(drive) => { + let path = install(&drive, path, force).await?; + Ok(path.to_owned_replacing()(drive)) + }, + Err(err) => Err(err), + } + }.into_stream() + }); + Ok(fut) +} + + +/// Validate path - pdz or pdx-dir. +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn validate_host_package(path: &Path) -> Result<()> { + use std::io::{Error, ErrorKind}; + + if !path.try_exists()? { + return Err(Error::new(ErrorKind::NotFound, "package not found").into()); + } + + (path.is_dir() || + path.extension() == Some(OsStr::new("pdz")) || + path.extension() == Some(OsStr::new("pdx"))) + .then_some(()) + .ok_or_else(|| Error::new(ErrorKind::InvalidData, "invalid package").into()) +} diff --git a/support/device/src/interface/async.rs b/support/device/src/interface/async.rs new file mode 100644 index 00000000..dccbbc50 --- /dev/null +++ b/support/device/src/interface/async.rs @@ -0,0 +1,62 @@ +use std::future::Future; + +use crate::device::command::Command; +use crate::error::Error; + + +pub trait Out: In { + // type Error: std::error::Error; + + fn send(&self, data: &[u8]) -> impl Future>; + + fn send_cmd(&self, cmd: Command) -> impl Future> { + async move { + let mut pre = 0; + if !matches!(cmd, Command::Echo { .. }) { + use crate::device::command::Switch; + + trace!("send cmd: echo off"); + let echo = Command::Echo { value: Switch::Off }; + pre = self.send(echo.with_break().as_bytes()).await?; + } + + trace!("send cmd: {cmd}"); + let sent = self.send(cmd.with_break().as_bytes()).await?; + Ok(pre + sent) + } + } +} + +pub trait In { + // type Error: std::error::Error = crate::error::Error; +} + +pub trait Interface: Out {} +impl Interface for T {} + + +// pub trait AsyncSend { +// fn send_cmd(&mut self, +// cmd: crate::device::command::Command) +// -> impl std::future::Future>; +// } + + +// mod ext { +// use super::*; + + +// impl AsyncSend for T +// where T: tokio::io::AsyncWriteExt, +// Self: Unpin +// { +// #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] +// async fn send_cmd(&mut self, cmd: crate::device::command::Command) -> Result { +// let cmd = cmd.with_break(); +// let bytes = cmd.as_bytes(); +// self.write_all(bytes).await?; +// self.flush().await?; +// Ok(bytes.len()) +// } +// } +// } diff --git a/support/device/src/interface/blocking.rs b/support/device/src/interface/blocking.rs new file mode 100644 index 00000000..cdced8b8 --- /dev/null +++ b/support/device/src/interface/blocking.rs @@ -0,0 +1,17 @@ +use crate::device::command::Command; +use crate::error::Error; + +pub trait Out: In { + // type Error: std::error::Error = crate::error::Error; + + // fn send(&self, data: &[u8]) -> Result; + fn send_cmd(&self, cmd: Command) -> Result; +} + +pub trait In { + // type Error: std::error::Error = crate::error::Error; +} + +pub trait Interface: In + Out {} +// impl + Out, Err> Interface for T {} +impl Interface for T {} diff --git a/support/device/src/interface/mod.rs b/support/device/src/interface/mod.rs new file mode 100644 index 00000000..8529986c --- /dev/null +++ b/support/device/src/interface/mod.rs @@ -0,0 +1,84 @@ +use futures::FutureExt; + +use crate::error::Error; + +pub mod blocking; +pub mod r#async; + + +pub enum Interface { + Usb(crate::usb::Interface), + Serial(crate::serial::Interface), +} + +impl From for Interface { + fn from(interface: crate::usb::Interface) -> Self { Self::Usb(interface) } +} + +impl From for Interface { + fn from(interface: crate::serial::Interface) -> Self { Self::Serial(interface) } +} + + +impl std::fmt::Display for Interface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Interface::Usb(interface) => interface.fmt(f), + Interface::Serial(interface) => interface.fmt(f), + } + } +} + + +impl std::fmt::Debug for Interface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Usb(_) => f.debug_tuple("Usb").finish(), + Self::Serial(i) => f.debug_tuple("Serial").field(i.info()).finish(), + } + } +} + + +impl r#async::Out for Interface + where crate::usb::Interface: r#async::Out, + crate::serial::Interface: r#async::Out +{ + #[inline(always)] + fn send(&self, data: &[u8]) -> impl futures::Future> { + match self { + Self::Usb(i) => i.send(data).left_future(), + Self::Serial(i) => i.send(data).right_future(), + } + } + + #[inline(always)] + async fn send_cmd(&self, cmd: crate::device::command::Command) -> Result { + match self { + Interface::Usb(i) => r#async::Out::send_cmd(i, cmd).await, + Interface::Serial(i) => r#async::Out::send_cmd(i, cmd).await, + } + } +} + +impl r#async::In for Interface + where crate::usb::Interface: r#async::In, + crate::serial::Interface: r#async::In +{ + // type Error = Error; +} + + +impl blocking::Out for Interface { + #[inline(always)] + fn send_cmd(&self, cmd: crate::device::command::Command) -> Result { + match self { + Interface::Usb(i) => blocking::Out::send_cmd(i, cmd), + Interface::Serial(i) => blocking::Out::send_cmd(i, cmd), + } + } +} + +impl blocking::In for Interface { + // type Error = crate::error::Error; +} diff --git a/support/device/src/lib.rs b/support/device/src/lib.rs new file mode 100644 index 00000000..0aecc2a3 --- /dev/null +++ b/support/device/src/lib.rs @@ -0,0 +1,35 @@ +#![allow(trivial_bounds)] +#![feature(trivial_bounds)] +#![feature(error_generic_member_access)] +#![feature(exit_status_error)] +#![feature(associated_type_defaults)] +#![cfg_attr(feature = "tracing", allow(unused_braces))] + +#[macro_use] +#[cfg(feature = "tracing")] +extern crate tracing; + +#[macro_use] +#[cfg(not(feature = "tracing"))] +extern crate log; + +pub extern crate serialport; + +pub mod error; +pub mod serial; +pub mod usb; +pub mod device; +pub mod mount; + +pub mod send; +pub mod install; +pub mod run; + +pub mod interface; + +pub mod retry; + + +pub const VENDOR_ID: u16 = 0x1331; +pub const PRODUCT_ID_DATA: u16 = 0x5740; +pub const PRODUCT_ID_STORAGE: u16 = 0x5741; diff --git a/support/device/src/mount/linux.rs b/support/device/src/mount/linux.rs new file mode 100644 index 00000000..8064f0ab --- /dev/null +++ b/support/device/src/mount/linux.rs @@ -0,0 +1,347 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::path::Path; +use std::path::PathBuf; +use std::future::Future; +use std::future::IntoFuture; + +use futures::FutureExt; +use udev::Enumerator; + +use crate::device::serial::SerialNumber; +use crate::error::Error; +use crate::device::Device; + + +#[derive(Debug, Clone)] +pub struct Volume { + /// FS mount point. + path: PathBuf, + + /// Partition node path, e.g.: `/dev/sda1`. + part_node: PathBuf, + + /// Disk node path, e.g.: `/dev/sda`. + disk_node: PathBuf, + + /// Device sysfs path. + dev_sysfs: PathBuf, +} + +impl Volume { + fn new(path: PathBuf, part: PathBuf, disk: PathBuf, dev_sysfs: PathBuf) -> Self { + Self { path, + part_node: part, + disk_node: disk, + dev_sysfs } + } +} + +impl std::fmt::Display for Volume { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.path.display().fmt(f) } +} + +impl Volume { + /// This volume's path. + pub fn path(&self) -> Cow<'_, Path> { self.path.as_path().into() } +} + + +mod unmount { + use futures::TryFutureExt; + + use super::*; + use crate::mount::Unmount; + use crate::mount::UnmountAsync; + + + impl Unmount for Volume { + #[cfg_attr(feature = "tracing", tracing::instrument())] + fn unmount_blocking(&self) -> Result<(), Error> { + use std::process::Command; + + + let res = eject(self).status() + .map_err(Error::from) + .and_then(|res| res.exit_ok().map_err(Error::from)) + .or_else(|err| -> Result<(), Error> { + unmount(self).status() + .map_err(Error::from) + .and_then(|res| res.exit_ok().map_err(Error::from)) + .map_err(|err2| Error::chain(err2, [err])) + }) + .or_else(move |err| -> Result<(), Error> { + udisksctl_unmount(self).status() + .map_err(Error::from) + .and_then(|res| res.exit_ok().map_err(Error::from)) + .map_err(|err2| Error::chain(err2, [err])) + }) + .or_else(move |err| -> Result<(), Error> { + udisks_unmount(self).status() + .map_err(Error::from) + .and_then(|res| res.exit_ok().map_err(Error::from)) + .map_err(|err2| Error::chain(err2, [err])) + }) + .inspect(|_| trace!("unmounted {self}")); + + // TODO: use `udisks_power_off` also as fallback for `udisksctl_power_off`: + Command::from(udisksctl_power_off(self)).status() + .map_err(Error::from) + .and_then(|res| res.exit_ok().map_err(Error::from)) + .map_err(move |err2| { + if let Some(err) = res.err() { + Error::chain(err2, [err]) + } else { + err2 + } + }) + } + } + + #[cfg(feature = "tokio")] + impl UnmountAsync for Volume { + #[cfg_attr(feature = "tracing", tracing::instrument())] + async fn unmount(&self) -> Result<(), Error> { + use tokio::process::Command; + use futures_lite::future::ready; + + + Command::from(eject(self)).status() + .map_err(Error::from) + .and_then(|res| ready(res.exit_ok().map_err(Error::from))) + .or_else(|err| { + Command::from(unmount(self)).status() + .map_err(|err2| Error::chain(err2, [err])) + .and_then(|res| { + ready(res.exit_ok().map_err(Error::from)) + }) + }) + .or_else(|err| { + Command::from(udisksctl_unmount(self)).status() + .map_err(|err2| { + Error::chain(err2, [err]) + }) + .and_then(|res| { + ready( + res.exit_ok() + .map_err(Error::from), + ) + }) + }) + .or_else(|err| { + Command::from(udisks_unmount(self)).status() + .map_err(|err2| { + Error::chain(err2, [err]) + }) + .and_then(|res| { + ready( + res.exit_ok() + .map_err(Error::from), + ) + }) + }) + .inspect_ok(|_| trace!("unmounted {self}")) + .then(|res| { + // TODO: use `udisks_power_off` also as fallback for `udisksctl_power_off`: + Command::from(udisksctl_power_off(self)).status() + .map_err(Error::from) + .and_then(|res| { + ready( + res.exit_ok() + .map_err(Error::from), + ) + }) + .map_err(|err2| { + if let Some(err) = res.err() { + Error::chain(err2, [err]) + } else { + err2 + } + }) + }) + .await + } + } + + + fn eject(vol: &Volume) -> std::process::Command { + let mut cmd = std::process::Command::new("eject"); + cmd.arg(vol.path().as_ref()); + cmd + } + + fn unmount(vol: &Volume) -> std::process::Command { + let mut cmd = std::process::Command::new("umount"); + cmd.arg(vol.path().as_ref()); + cmd + } + + fn udisksctl_unmount(vol: &Volume) -> std::process::Command { + let mut cmd = std::process::Command::new("udisksctl"); + cmd.args(["unmount", "--no-user-interaction", "-b"]); + cmd.arg(&vol.part_node); + cmd + } + + fn udisksctl_power_off(vol: &Volume) -> std::process::Command { + let mut cmd = std::process::Command::new("udisksctl"); + cmd.args(["power-off", "--no-user-interaction", "-b"]); + cmd.arg(&vol.disk_node); + cmd + } + + fn udisks_unmount(vol: &Volume) -> std::process::Command { + let mut cmd = std::process::Command::new("udisks"); + cmd.arg("--unmount"); + cmd.arg(&vol.part_node); + cmd + } + + fn udisks_power_off(vol: &Volume) -> std::process::Command { + let mut cmd = std::process::Command::new("udisks"); + cmd.arg("--detach"); + cmd.arg(&vol.disk_node); + cmd + } + + // NOTE: mb. try to use `udisks`, that's existing in Ubuntu. + // udisksctl unmount -b /dev/sdc1 && udisksctl power-off -b /dev/sdc + // udisks --unmount /dev/sdb1 && udisks --detach /dev/sdb +} + + +#[cfg_attr(feature = "tracing", tracing::instrument(fields(dev = dev.as_ref().serial_number())))] +pub async fn volume_for(dev: Info) -> Result + where Info: AsRef { + let sysfs = dev.as_ref().sysfs_path(); + let mut enumerator = enumerator()?; + enumerator.add_syspath(sysfs)?; + + if let Some(sn) = dev.as_ref().serial_number() { + enumerator.match_property("ID_SERIAL_SHORT", sn)?; + } + + let mounts = lfs_core::read_mountinfo()?; + enumerator.scan_devices()? + .filter_map(|udev| { + udev.devtype() + .filter(|ty| *ty == OsStr::new("partition")) + .is_some() + .then(move || udev.devnode().map(Path::to_path_buf).map(|node| (udev, node))) + }) + .flatten() + .find_map(|(udev, node)| { + mounts.iter() + .find(|inf| Path::new(inf.fs.as_str()) == node.as_path()) + .map(|inf| (udev, node, inf)) + }) + .and_then(|(udev, node, minf)| { + let disk = udev.parent() + .filter(is_disk) + .or_else(|| udev.parent().map(|d| d.parent().filter(is_disk)).flatten()) + .and_then(|dev| dev.devnode().map(ToOwned::to_owned)); + let sysfs = PathBuf::from(sysfs); + disk.map(move |disk| Volume::new(minf.mount_point.clone(), node, disk, sysfs)) + }) + .ok_or_else(|| Error::not_found()) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument(skip(devs)))] +pub async fn volumes_for_map(devs: I) -> Result>, Error> + where I: IntoIterator { + let mounts = lfs_core::read_mountinfo()?; + + if mounts.is_empty() { + return Ok(devs.into_iter().map(|dev| (dev, None)).collect()); + } + + let mut enumerator = enumerator()?; + + let udevs: Vec<_> = enumerator.scan_devices()? + .filter(is_partition) + .filter_map(|dev| { + if let Some(sn) = dev.property_value("ID_SERIAL_SHORT") { + let sn = sn.to_string_lossy().to_string(); + Some((dev, sn)) + } else { + if let Some(sn) = dev.property_value("ID_SERIAL") { + let sn: Result = + sn.to_string_lossy().as_ref().try_into(); + sn.ok().map(|sn| (dev, sn.to_string())) + } else { + None + } + } + }) + .collect(); + + if udevs.is_empty() { + return Ok(devs.into_iter().map(|dev| (dev, None)).collect()); + } + + let mut devs = devs.into_iter().filter_map(|dev| { + if let Some(sn) = dev.info().serial_number().map(ToOwned::to_owned) { + Some((dev, sn)) + } else { + None + } + }); + + let result = + devs.map(|(dev, ref sna)| { + let node = + udevs.iter() + .find_map(|(inf, snb)| { + (sna == snb).then(|| inf.devnode()) + .flatten() + .map(ToOwned::to_owned) + .map(|dn| (inf, dn)) + }) + .and_then(|(udev, node)| { + mounts.iter() + .find(|inf| Path::new(inf.fs.as_str()) == node) + .and_then(|inf| { + let disk = udev.parent() + .filter(is_disk) + .or_else(|| udev.parent().map(|d| d.parent().filter(is_disk)).flatten()) + .and_then(|dev| dev.devnode().map(ToOwned::to_owned)); + + let sysfs = dev.info().sysfs_path().to_owned(); + disk.map(move |disk| Volume::new(inf.mount_point.clone(), node, disk, sysfs)) + }) + }); + (dev, node) + }) + .collect(); + Ok(result) +} + + +// TODO: this is needed too: +// pub fn volumes_for<'i, I: 'i>( +// devs: I) +// -> Result>, &'i Device)>, Error> +// where I: IntoIterator { +// // +// Ok(vec![(futures::future::lazy(|_| todo!()).into_future(), &todo!())].into_iter()) +// } + + +fn enumerator() -> Result { + let mut enumerator = udev::Enumerator::new()?; + // filter only PD devices: + enumerator.match_property("ID_VENDOR", "Panic")?; + enumerator.match_property("ID_MODEL", "Playdate")?; + Ok(enumerator) +} + + +fn is_partition(dev: &udev::Device) -> bool { + dev.devtype() + .filter(|ty| *ty == OsStr::new("partition")) + .is_some() +} + +fn is_disk(dev: &udev::Device) -> bool { dev.devtype().filter(|ty| *ty == OsStr::new("disk")).is_some() } diff --git a/support/device/src/mount/mac.rs b/support/device/src/mount/mac.rs new file mode 100644 index 00000000..63be857a --- /dev/null +++ b/support/device/src/mount/mac.rs @@ -0,0 +1,271 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; +use futures::Future; +use futures::FutureExt; +use serde::Deserialize; +use crate::device::Device; +use crate::error::Error; + + +pub const VENDOR_ID_ENC: &str = const_hex::const_encode::<2, true>(&crate::VENDOR_ID.to_be_bytes()).as_str(); + + +#[derive(Debug, Clone)] +pub struct Volume { + path: PathBuf, +} + +impl From for Volume { + fn from(path: PathBuf) -> Self { Self { path } } +} + +impl std::fmt::Display for Volume { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.path.display().fmt(f) } +} + +impl Volume { + /// This volume's path. + pub fn path(&self) -> Cow<'_, Path> { self.path.as_path().into() } +} + + +mod unmount { + use super::*; + use crate::mount::Unmount; + use crate::mount::UnmountAsync; + + + impl Unmount for Volume { + #[cfg_attr(feature = "tracing", tracing::instrument())] + fn unmount_blocking(&self) -> Result<(), Error> { + cmd(self).status()? + .exit_ok() + .map(|_| trace!("unmounted {self}")) + .map_err(Into::into) + } + } + + #[cfg(feature = "tokio")] + impl UnmountAsync for Volume { + #[cfg_attr(feature = "tracing", tracing::instrument())] + async fn unmount(&self) -> Result<(), Error> { + tokio::process::Command::from(cmd(self)).status() + .await? + .exit_ok() + .map(|_| trace!("unmounted {self}")) + .map_err(Into::into) + } + } + + fn cmd(vol: &Volume) -> std::process::Command { + let mut cmd = std::process::Command::new("diskutil"); + cmd.arg("eject"); + cmd.arg(vol.path().as_ref()); + cmd + } +} + + +#[derive(Debug)] +pub struct SpusbInfo + where Fut: Future> { + pub name: String, + pub serial: String, + pub volume: Fut, +} + + +#[cfg_attr(feature = "tracing", tracing::instrument(fields(dev = dev.as_ref().serial_number())))] +pub async fn volume_for(dev: Info) -> Result + where Info: AsRef { + if let Some(sn) = dev.as_ref().serial_number() { + let res = spusb(move |ref info| info.serial_num == sn).map(|mut iter| iter.next().map(|info| info.volume)); + match res { + Ok(None) => Err(Error::not_found()), + Ok(Some(fut)) => Ok(fut), + Err(err) => Err(err), + } + } else { + Err(Error::not_found()) + }?.await + .map(Volume::from) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(skip(devs)))] +pub async fn volumes_for_map(devs: I) -> Result>, Error> + where I: IntoIterator { + let mut devs = devs.into_iter() + .filter_map(|dev| { + if let Some(sn) = dev.info().serial_number().map(ToOwned::to_owned) { + Some((dev, sn)) + } else { + None + } + }) + .collect::>(); + + let mut results = HashMap::with_capacity(devs.len()); + + for info in spusb(|_| true)? { + let i = devs.iter() + .enumerate() + .find(|(_, (_, sn))| &info.serial == sn) + .map(|(i, _)| i); + + if let Some(i) = i { + match info.volume.await { + Ok(vol) => { + let (dev, _) = devs.remove(i); + results.insert(dev, Some(Volume { path: vol })); + }, + Err(err) => error!("{err}"), + } + } + } + + results.extend(devs.into_iter().map(|(dev, _)| (dev, None))); + + Ok(results) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(skip(devs)))] +pub fn volumes_for<'i, I: 'i>( + devs: I) + -> Result>, &'i Device)>, Error> + where I: IntoIterator { + let devs = devs.into_iter() + .filter_map(|dev| dev.info().serial_number().map(|sn| (dev, sn))) + .collect::>(); + + spusb(|_| true).map(move |iter| { + iter.filter_map(move |info| { + devs.iter() + .find(|(_, sn)| info.serial == *sn) + .map(|(dev, _)| (info.volume, *dev)) + }) + }) +} + + +/// Call `system_profiler -json SPUSBDataType` +#[cfg_attr(feature = "tracing", tracing::instrument(skip(filter)))] +fn spusb(filter: F) + -> Result>>>, Error> + where F: FnMut(&DeviceInfo) -> bool { + use std::process::Command; + + let output = Command::new("system_profiler").args(["-json", "SPUSBDataType"]) + .output()?; + output.status.exit_ok()?; + + let data: SystemProfilerResponse = serde_json::from_reader(&output.stdout[..])?; + + let result = data.data + .into_iter() + .filter_map(|c| c.items) + .flatten() + .filter(|item| item.vendor_id == VENDOR_ID_ENC) + .filter(filter) + .filter_map(|item| { + let DeviceInfo { name, + serial_num: serial, + media, + .. } = item; + let volume = media.map(|media| { + media.into_iter() + .flat_map(|root| root.volumes.into_iter()) + .filter_map(|par| { + if let Some(path) = par.mount_point { + trace!("found mount-point: {}", path.display()); + Some(futures_lite::future::ready(Ok(path)).left_future()) + } else { + let path = Path::new("/Volumes").join(&par.name); + if path.exists() { + trace!("existing, by name: {}", path.display()); + Some(futures_lite::future::ready(Ok(path)).left_future()) + } else if par.volume_uuid.is_some() { + trace!("not mounted yet, create resolver fut"); + Some(mount_point_for_partition(par).right_future()) + } else { + None + } + } + }) + .next() + }) + .flatten(); + volume.map(|volume| SpusbInfo { name, serial, volume }) + }); + Ok(result) +} + + +/// Calls `diskutil info -plist {partition.volume_uuid}` +#[cfg_attr(feature = "tracing", tracing::instrument(skip(par), fields(par.name = par.name.as_str())))] +async fn mount_point_for_partition(par: MediaPartitionInfo) -> Result { + use std::process::Command; + + if let Some(volume_uuid) = par.volume_uuid.as_deref() { + let output = Command::new("diskutil").args(["info", "-plist"]) + .arg(volume_uuid) + .output()?; + output.status.exit_ok()?; + + let info: DiskUtilResponse = plist::from_bytes(output.stdout.as_slice())?; + info.mount_point + .ok_or(Error::MountNotFound(format!("{} {}", &par.name, &par.bsd_name))) + .map(PathBuf::from) + } else { + Err(Error::MountNotFound(format!("{} {}", &par.name, &par.bsd_name))) + } +} + + +#[derive(Deserialize, Debug)] +struct DiskUtilResponse { + #[serde(rename = "MountPoint")] + mount_point: Option, +} + + +#[derive(Deserialize, Debug)] +struct SystemProfilerResponse { + #[serde(rename = "SPUSBDataType")] + data: Vec, +} + + +#[derive(Deserialize, Debug)] +struct ControllerInfo { + #[serde(rename = "_items")] + items: Option>, +} + +#[derive(Deserialize, Debug)] +#[allow(dead_code)] +pub struct DeviceInfo { + #[serde(rename = "_name")] + pub name: String, + pub serial_num: String, + pub vendor_id: String, + + #[serde(rename = "Media")] + pub media: Option>, +} + + +#[derive(Deserialize, Debug)] +pub struct DeviceMediaInfo { + volumes: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct MediaPartitionInfo { + #[serde(rename = "_name")] + name: String, + bsd_name: String, + volume_uuid: Option, + mount_point: Option, +} diff --git a/support/device/src/mount/methods.rs b/support/device/src/mount/methods.rs new file mode 100644 index 00000000..1c4eef3e --- /dev/null +++ b/support/device/src/mount/methods.rs @@ -0,0 +1,384 @@ +use std::future::Future; +use std::time::Duration; + +use futures::stream::FuturesUnordered as Unordered; +use futures::{FutureExt, Stream, StreamExt, TryFutureExt}; + +use crate::device::query::Query; +use crate::device::query::Value as QueryValue; +use crate::device::serial::SerialNumber as Sn; +use crate::device::{wait_mode_storage, wait_mode_data, Device}; +use crate::error::Error; +use crate::interface::r#async::Out; +use crate::mount::{MountAsync, MountHandle}; +use crate::mount::MountedDevice; +use crate::mount::volume::volumes_for_map; +use crate::retry::{DefaultIterTime, Retries, IterTime}; +use crate::usb::discover::devices_storage; +use crate::usb; +use crate::serial::{self, dev_with_port}; +use crate::interface; + + +type Result = std::result::Result; + + +/// Recommended total time for retries is 30 seconds or more. +/// +/// ```ignore +/// let retry = Retries::new(Duration::from_secs(1), Duration::from_secs(60)); +/// mount::wait_fs_available(drive, retry).await?; +/// ``` +#[cfg_attr(feature = "tracing", tracing::instrument(fields(dev = mount.device.to_string(), + mount = mount.handle.volume().path().as_ref().display().to_string(), + )))] +pub async fn wait_fs_available(mount: &MountedDevice, retry: Retries) -> Result + where T: Clone + std::fmt::Debug + IterTime { + let total = &retry.total; + let iter_ms = retry.iters.interval(total); + let retries_num = total.as_millis() / iter_ms.as_millis(); + debug!("retries: {retries_num} * {iter_ms:?} ≈ {total:?}."); + + let mut counter = retries_num; + let mut interval = tokio::time::interval(iter_ms); + + let check = || { + mount.handle + .path() + .try_exists() + .inspect_err(|err| debug!("{err}")) + .ok() + .unwrap_or_default() + .then(|| { + let path = mount.handle.path(); + match std::fs::read_dir(path).inspect_err(|err| debug!("{err}")) { + // then find first dir entry: + Ok(entries) => entries.into_iter().flatten().next().is_some(), + _ => false, + } + }) + .unwrap_or_default() + }; + + if check() { + trace!("filesystem available at {}", mount.handle.path().display()); + return Ok(()); + } + + while { + counter -= 1; + counter + } != 0 + { + interval.tick().await; + + if check() { + return Ok(()); + } else { + trace!( + "{dev}: waiting filesystem availability, try: {i}/{retries_num}", + dev = mount.device, + i = retries_num - counter, + ); + } + } + + Err(Error::timeout(format!( + "{dev}: filesystem not available at {path} after {retries_num} retries", + dev = mount.device, + path = mount.handle.path().display(), + ))) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn mount(query: Query) -> Result>> { + match query.value { + Some(QueryValue::Path(port)) => { + let fut = mount_by_port_name(port.display().to_string()).await? + .left_stream(); + Ok(fut) + }, + Some(QueryValue::Com(port)) => { + let fut = mount_by_port_name(format!("COM{port}")).await?.left_stream(); + Ok(fut) + }, + Some(QueryValue::Serial(sn)) => Ok(mount_by_sn_mb(Some(sn)).await?.right_stream()), + _ => Ok(mount_by_sn_mb(None).await?.right_stream()), + } +} + + +/// Switch between stream methods `mount` and `mount then wait_fs_available`, +/// depending on `wait` parameter. +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn mount_and(query: Query, wait: bool) -> Result>> { + let fut = + mount(query).await?.flat_map(move |res| { + async move { + match res { + Ok(drive) => { + if wait { + let retry = + Retries::new(Duration::from_millis(500), Duration::from_secs(60)); + wait_fs_available(&drive, retry).await? + } + Ok(drive) + }, + Err(err) => Err(err), + } + }.into_stream() + }); + Ok(fut) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn mount_by_sn_mb(sn: Option) -> Result>>> { + let devices = usb::discover::devices_with(sn)?; + let mounting = devices.map(mount_dev); + + let futures = Unordered::new(); + for dev in mounting { + futures.push(dev?); + } + + if futures.is_empty() { + Err(Error::not_found()) + } else { + Ok(futures) + } +} + + +#[cfg_attr(feature = "tracing", tracing::instrument(fields(port = port.as_ref())))] +pub async fn mount_by_port_name>( + port: S) + -> Result>>> { + let port = port.as_ref(); + let existing = serial::discover::ports().map(|ports| { + ports.into_iter() + .find(|p| p.port_name == port) + .map(serial::Interface::new) + }); + + let futures = Unordered::new(); + + let err_not_found = || futures_lite::future::ready(Err(Error::not_found())); + + match existing { + Ok(Some(port)) => { + if let serialport::SerialPortType::UsbPort(serialport::UsbPortInfo { serial_number: Some(ref sn), + .. }) = port.info().port_type + { + let dev = Sn::try_from(sn.as_str()).map_err(Error::from) + .and_then(|sn| usb::discover::device(&sn)); + match dev { + Ok(mut dev) => { + dev.set_interface(interface::Interface::Serial(port)); + futures.push(mount_dev(dev)?.left_future()); + }, + Err(err) => { + let name = port.info().port_name.as_str(); + error!("Unable to map specified port {name} to device: {err}"); + port.mount().await?; + futures.push(err_not_found().right_future()); + }, + } + } + }, + Ok(None) => { + match dev_with_port(port).await { + Ok(dev) => futures.push(mount_dev(dev)?.left_future()), + Err(err) => { + let name = port; + error!("Unable to map specified port {name} to device: {err}"); + let port = serial::open(name)?; + let interface = serial::Interface::new_with(port, Some(name.to_string())); + interface.send_cmd(crate::device::command::Command::Datadisk) + .await?; + futures.push(err_not_found().right_future()); + }, + } + }, + Err(err) => { + error!("{err}"); + match dev_with_port(port).await { + Ok(dev) => futures.push(mount_dev(dev)?.left_future()), + Err(err) => { + let name = port; + error!("Unable to map specified port {name} to device: {err}"); + let port = serial::open(name)?; + let interface = serial::Interface::new_with(port, Some(name.to_string())); + interface.send_cmd(crate::device::command::Command::Datadisk) + .await?; + futures.push(err_not_found().right_future()); + }, + } + }, + } + + if futures.is_empty() { + Err(Error::not_found()) + } else { + Ok(futures) + } +} + + +#[cfg_attr(feature = "tracing", tracing::instrument(fields(dev = dev.info().serial_number())))] +fn mount_dev(mut dev: Device) -> Result>> { + let retry = Retries::::default(); + let mut retry_wait_mount_point = retry.clone(); + retry_wait_mount_point.total += Duration::from_secs(40); + + trace!("mounting {dev}"); + let fut = match dev.mode_cached() { + usb::mode::Mode::Data => { + trace!("create sending fut"); + async move { + dev.open()?; + dev.interface()? + .send_cmd(crate::device::command::Command::Datadisk) + .await?; + dev.close(); + Ok(dev) + }.and_then(|dev| wait_mode_storage(dev, retry)) + .left_future() + }, + usb::mode::Mode::Storage => futures_lite::future::ready(Ok(dev)).right_future(), + mode => return Err(Error::WrongState(mode)), + }; + Ok(fut.and_then(|dev| wait_mount_point(dev, retry_wait_mount_point))) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument(fields(dev = dev.info().serial_number())))] +async fn wait_mount_point(dev: Device, retry: Retries) -> Result + where T: Clone + std::fmt::Debug + IterTime { + let total = &retry.total; + let iter_ms = retry.iters.interval(total); + let retries_num = total.as_millis() / iter_ms.as_millis(); + debug!("retries: {retries_num} * {iter_ms:?} ≈ {total:?}."); + + let mut counter = retries_num; + let mut interval = tokio::time::interval(iter_ms); + + let sn = dev.info() + .serial_number() + .ok_or_else(|| Error::DeviceSerial { source: "unknown".into() })? + .to_owned(); + + while { + counter -= 1; + counter + } != 0 + { + interval.tick().await; + + let mode = dev.mode_cached(); + trace!( + "waiting mount point availability: {sn}, current: {mode}, try: {}/{retries_num}", + retries_num - counter + ); + + let vol = crate::mount::volume::volume_for(&dev).await + .map_err(|err| debug!("ERROR: {err}")) + .ok(); + if let Some(vol) = vol { + debug!("{sn} mounted, volume found: '{vol}'"); + let handle = MountHandle::new(vol, false); + let mounted = MountedDevice::new(dev, handle); + return Ok(mounted); + } else { + debug!("mount point still not found, waiting...") + } + } + + Err(Error::usb_timeout(dev)) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn unmount(query: Query) -> Result>> { + match query.value { + Some(QueryValue::Path(path)) => { + // TODO: Check query is path and this is mounted volume. + // check is `path` is a a path of existing __volume__, + // try find device behind the volume, + // unmount the volume anyway + todo!("unmount dev by vol path: {}", path.display()) + }, + Some(QueryValue::Com(_)) => todo!("ERROR: not supported (impossible)"), + Some(QueryValue::Serial(sn)) => unmount_mb_sn(Some(sn)), + _ => unmount_mb_sn(None), + }.await +} + +/// Unmount device(s), then wait for state change to [`Data`][usb::mode::Mode::Data]. +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn unmount_and_wait(query: Query, retry: Retries) -> Result>> + where T: Clone + std::fmt::Debug + IterTime { + let stream = Unordered::new(); + unmount(query).await? + .for_each_concurrent(4, |(dev, res)| { + if let Some(err) = res.err() { + error!("{dev}: {err}") + } + stream.push(wait_mode_data(dev, retry.clone())); + futures_lite::future::ready(()) + }) + .await; + + trace!("Waiting state change for {} devices.", stream.len()); + Ok(stream) +} + +/// Switch between stream methods `unmount` and `unmount_and_wait`, +/// depending on `wait` parameter. +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn unmount_and(query: Query, wait: bool) -> Result>> { + let results = if wait { + let retry = Retries::::default(); + unmount_and_wait(query, retry).await?.left_stream() + } else { + unmount(query).await? + .map(|(dev, res)| res.map(|_| dev)) + .right_stream() + }; + + Ok(results) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn unmount_mb_sn(sn: Option) -> Result>> { + let devs = devices_storage()?.filter(move |dev| { + sn.as_ref() + .filter(|qsn| dev.info().serial_number().filter(|ref s| qsn.eq(s)).is_some()) + .is_some() || + sn.is_none() + }) + .inspect(|dev| trace!("Unmounting {dev}")); + + let unmounting = volumes_for_map(devs).await? + .into_iter() + .filter_map(|(dev, vol)| vol.map(|vol| (dev, vol))) + .inspect(|(dev, vol)| trace!("Unmounting {dev}: {vol}")) + .map(|(dev, vol)| { + let h = MountHandle::new(vol, false); + MountedDevice::new(dev, h) + }) + .map(move |mut dev| { + use crate::mount::UnmountAsync; + async move { + dev.device.close(); + let res = dev.unmount().await; + (dev.device, res) + } + }) + .collect::>(); + + trace!("Unmounting {} devices.", unmounting.len()); + Ok(unmounting) +} diff --git a/support/device/src/mount/mod.rs b/support/device/src/mount/mod.rs new file mode 100644 index 00000000..59d02988 --- /dev/null +++ b/support/device/src/mount/mod.rs @@ -0,0 +1,166 @@ +use std::borrow::Cow; +use std::ops::Deref; +use std::ops::DerefMut; +use std::path::Path; + +use crate::device::Device; +use crate::error::Error; + + +#[cfg(target_os = "macos")] +#[path = "mac.rs"] +pub mod volume; +#[cfg(target_os = "windows")] +#[path = "win.rs"] +pub mod volume; +#[cfg(target_os = "linux")] +#[path = "linux.rs"] +pub mod volume; + +mod methods; +pub use methods::*; + + +// TODO: If unmount fails, do warn!("Please press 'A' on the Playdate to exit Data Disk mode.") + + +// TODO: MountError for this module + + +pub trait Volume { + /// This volume's path. + fn path(&self) -> Cow<'_, Path>; +} + +pub trait Unmount { + /// Unmount this volume. Blocking. + fn unmount_blocking(&self) -> Result<(), Error>; +} + +pub trait UnmountAsync { + /// Unmount this volume. + fn unmount(&self) -> impl std::future::Future>; +} + +pub trait Mount { + /// Mount this volume. Blocking. + fn mount_blocking(&self) -> Result<(), Error>; +} + +pub trait MountAsync { + fn mount(&self) -> impl std::future::Future>; +} + + +impl Mount for Device { + fn mount_blocking(&self) -> Result<(), Error> { + use crate::interface::blocking::Out; + use crate::device::command::Command; + + self.interface()?.send_cmd(Command::Datadisk)?; + Ok(()) + } +} + +impl MountAsync for Device { + async fn mount(&self) -> Result<(), Error> { self.interface()?.mount().await } +} + + +impl MountAsync for T where T: crate::interface::r#async::Out { + async fn mount(&self) -> Result<(), Error> { + self.send_cmd(crate::device::command::Command::Datadisk).await?; + Ok(()) + } +} + +impl Mount for T where T: crate::interface::blocking::Out { + fn mount_blocking(&self) -> Result<(), Error> { + self.send_cmd(crate::device::command::Command::Datadisk)?; + Ok(()) + } +} + + +impl UnmountAsync for T where T: crate::interface::r#async::Out { + async fn unmount(&self) -> Result<(), Error> { + self.send_cmd(crate::device::command::Command::Datadisk).await?; + Ok(()) + } +} + +impl Unmount for T where T: crate::interface::blocking::Out { + fn unmount_blocking(&self) -> Result<(), Error> { + self.send_cmd(crate::device::command::Command::Datadisk)?; + Ok(()) + } +} + + +pub struct MountedDevice { + pub device: Device, + pub handle: MountHandle, +} + +impl Unmount for MountedDevice { + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(dev = self.info().serial_number(), mount = self.handle.volume().path().as_ref().display().to_string())))] + fn unmount_blocking(&self) -> Result<(), Error> { + ::unmount_blocking(&self.handle.volume) + } +} + +impl UnmountAsync for MountedDevice where volume::Volume: UnmountAsync { + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(dev = self.info().serial_number(), mount = self.handle.volume().path().as_ref().display().to_string())))] + fn unmount(&self) -> impl std::future::Future> { + ::unmount(&self.handle.volume) + } +} + +impl MountedDevice { + pub fn new(device: Device, handle: MountHandle) -> Self { Self { device, handle } } +} + +impl Deref for MountedDevice { + type Target = Device; + fn deref(&self) -> &Self::Target { &self.device } +} + +impl DerefMut for MountedDevice { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.device } +} + + +pub struct MountHandle { + volume: volume::Volume, + pub unmount_on_drop: bool, +} + +impl MountHandle { + pub fn new(volume: volume::Volume, unmount_on_drop: bool) -> Self { + Self { volume, + unmount_on_drop } + } + + pub fn path(&self) -> Cow<'_, Path> { self.volume.path() } + pub fn volume(&self) -> &volume::Volume { &self.volume } + + pub fn unmount(mut self) { + self.unmount_on_drop = true; + drop(self) + } +} + +impl Drop for MountHandle { + fn drop(&mut self) { + if self.unmount_on_drop { + trace!("Unmounting {} by drop", self.volume); + let _ = self.volume + .unmount_blocking() + .map_err(|err| { + error!("{err}"); + info!("Please press 'A' on the Playdate to exit Data Disk mode."); + }) + .ok(); + } + } +} diff --git a/support/device/src/mount/win.rs b/support/device/src/mount/win.rs new file mode 100644 index 00000000..cd02dbe5 --- /dev/null +++ b/support/device/src/mount/win.rs @@ -0,0 +1,578 @@ +extern crate windows; +use std::borrow::Cow; +use std::collections::HashMap; +use std::ffi::CString; +use std::path::{Path, PathBuf}; +use std::pin::Pin; + +use windows::core::PCSTR; +use windows::Win32::Storage::FileSystem::GetLogicalDrives; +use windows::Win32::Storage::FileSystem::GetVolumeInformationA; +use windows::Win32::Storage::FileSystem::IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS; +use windows::Win32::System::IO::DeviceIoControl; +use windows::Win32::Foundation::HANDLE; + +use crate::device::serial::SerialNumber; +use crate::error::Error; +use crate::usb::mode::{DeviceMode, Mode}; +use crate::device::Device; + + +#[derive(Debug, Clone)] +pub struct Volume { + letter: char, + disk_number: Option, + serial_number: Option, +} + +impl std::fmt::Display for Volume { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.letter.fmt(f) } +} + +impl Volume { + /// This volume's path. + pub fn path(&self) -> Cow<'_, Path> { PathBuf::from(format!("{}:", self.letter)).into() } +} + +mod unmount { + use futures::FutureExt; + use futures::TryFutureExt; + + use super::*; + use crate::mount::Unmount; + use crate::mount::UnmountAsync; + + + impl Unmount for Volume { + #[cfg_attr(feature = "tracing", tracing::instrument())] + fn unmount_blocking(&self) -> Result<(), Error> { + winapi::unmount(self.letter).or_else(|err| { + if std::env::var_os("SHELL").is_some() { + eject_sh(self.letter).status() + .map_err(Error::from) + .and_then(|res| { + res.exit_ok().map_err(Error::from) + }) + .map_err(|err2| Error::chain(err2, [err])) + } else { + Err(err) + } + }) + .or_else(|err| { + eject_pw(self.letter).status() + .map_err(Error::from) + .and_then(|res| res.exit_ok().map_err(Error::from)) + .map_err(|err2| Error::chain(err2, [err])) + }) + } + } + + #[cfg(feature = "tokio")] + impl UnmountAsync for Volume { + #[cfg_attr(feature = "tracing", tracing::instrument())] + async fn unmount(&self) -> Result<(), Error> { + use tokio::process::Command; + use futures_lite::future::ready; + + futures::future::lazy(|_| winapi::unmount(self.letter)).or_else(|err| { + if std::env::var_os("SHELL").is_some() { + Command::from(eject_sh(self.letter)).status() + .map_err(|err2| Error::chain(err2, [err])) + .and_then(|res| ready(res.exit_ok().map_err(Error::from))) + .left_future() + } else { + ready(Err(err)).right_future() + } + }) + .or_else(|err| { + Command::from(eject_pw(self.letter)).status() + .map_err(|err2| Error::chain(err2, [err])) + .and_then(|res| ready(res.exit_ok().map_err(Error::from))) + }) + .await + } + } + + + fn eject_sh(letter: char) -> std::process::Command { + let string = to_vol_path_short(letter); + let mut cmd = std::process::Command::new("eject"); + cmd.arg(string); + cmd + } + + fn eject_pw(letter: char) -> std::process::Command { + // let arg = format!("(New-Object -comObject Shell.Application).NameSpace(17).ParseName('{letter}:').InvokeVerb('Eject') | Wait-Process"); + let arg = format!("(new-object -COM Shell.Application).NameSpace(17).ParseName('{letter}:').InvokeVerb('Eject') | Wait-Process"); + let mut cmd = std::process::Command::new("powershell"); + cmd.arg(arg); + cmd + } +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn volume_for(dev: &Device) -> Result { + if !matches!(dev.info().mode(), Mode::Storage) { + return Err(Error::not_found()); + } + + let sn = dev.info().serial_number().ok_or_else(|| { + Error::DeviceSerial { source: crate::device::serial::DeviceSerialFormatError::from("missed") } + })?; + let sn = SerialNumber::try_from(sn)?; + let dev_addr = dev.info().device_address() as u32; + enumerate_volumes().find(move |vol| is_that_vol(&sn, dev_addr, &vol)) + .ok_or_else(|| Error::not_found()) +} + +#[cfg_attr(feature = "tracing", tracing::instrument())] +fn is_that_vol(dev_sn: &SerialNumber, device_address: u32, vol: &Volume) -> bool { + if let Some(vol_sn) = vol.serial_number.as_ref() { + vol_sn == dev_sn + } else { + vol.disk_number == Some(device_address) + } +} + +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub fn volumes_for(devs: &[Device]) -> impl Iterator { + enumerate_volumes().filter_map(|vol| { + devs.into_iter() + .find(|dev| { + if let Some(sn) = dev.info().serial_number() { + if let Ok(sn) = SerialNumber::try_from(sn).inspect_err(|err| error!("{err}")) { + let dev_addr = dev.info().device_address() as u32; + return is_that_vol(&sn, dev_addr, &vol); + } + } + false + }) + .map(|dev| (vol, dev)) + }) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument(skip(devs)))] +pub async fn volumes_for_map(devs: I) -> Result>, Error> + where I: IntoIterator { + let mut devs: Vec<_> = devs.into_iter().collect(); + let mut results = HashMap::with_capacity(devs.len()); + let vols = enumerate_volumes().filter_map(|vol| { + let i = devs.iter() + .enumerate() + .find(|(_, dev)| { + if let Some(sn) = dev.info().serial_number() { + if let Ok(sn) = + SerialNumber::try_from(sn).inspect_err(|err| error!("{err}")) + { + let dev_addr = dev.info().device_address() as u32; + return is_that_vol(&sn, dev_addr, &vol); + } + } + false + }) + .map(|(i, _)| i); + if let Some(i) = i { + let dev = devs.remove(i); + Some((dev, Some(vol))) + } else { + None + } + }); + results.extend(vols); + results.extend(devs.into_iter().map(|dev| (dev, None))); + Ok(results) +} + + +/// Enumerate all available mounted volumes, +/// filtered by: +/// - that is Playdate +/// - that are has SN __or__ DN. +#[cfg_attr(feature = "tracing", tracing::instrument())] +fn enumerate_volumes() -> impl Iterator { + let letters = winapi::enumerate_volumes(); + + letters.filter_map(|letter| { + // TODO: remove this check: + winapi::get_volume_name(letter).inspect_err(|err| debug!("{letter}: {err}")) + .ok()? + .starts_with("PLAYDATE") + .then_some(letter) + }) + .map(|letter| { + let sn = ps::vol_sn(letter).inspect_err(|err| debug!("{letter}: {err}")) + .ok(); + // Get DN if no SN: + let dn = sn.is_none() + .then(|| { + ps::vol_dn(letter).inspect_err(|err| debug!("{letter}: {err}")) + .ok() + }) + .flatten(); + + trace!("Seems to {letter} is a Playdate: {dn:?}, {sn:?}"); + Volume { letter, + disk_number: dn, + serial_number: sn } + }) + .map(|mut vol| { + if vol.disk_number.is_none() { + let dn = winapi::get_disk_number(vol.letter).inspect_err(|err| debug!("{}: {err}", vol.letter)) + .ok(); + if let Some(dn) = dn { + vol.disk_number = Some(dn); + } + } + vol + }) + .filter(|vol| vol.disk_number.is_some() || vol.serial_number.is_some()) +} + + +mod winapi { + use super::*; + + #[cfg_attr(feature = "tracing", tracing::instrument())] + pub fn enumerate_volumes() -> impl Iterator { + const LETTERS: &'static str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let mask = unsafe { GetLogicalDrives() }; + let letters = (0..(std::mem::size_of_val(&mask) * 8)).into_iter() + .filter(move |i| (1 << *i as u32) & mask != 0) + .filter_map(|i| LETTERS.chars().nth(i)); + letters + } + + #[cfg_attr(feature = "tracing", tracing::instrument())] + pub fn get_volume_name(letter: char) -> Result { + let (_string, s) = pcstr_short(letter); + let mut name_buf = vec![0; 1000]; + + let mut serial = 0_u32; + unsafe { + GetVolumeInformationA(s, Some(&mut name_buf), Some(&mut serial), None, None, None) + } + .map_err(std::io::Error::from)?; + + let drive_name = std::str::from_utf8(&name_buf)?.trim(); + + let temp_drive_name = (!drive_name.is_empty()).then_some(drive_name) + .unwrap_or("unnamed"); + trace!("found drive: {letter} '{temp_drive_name}' ({serial})"); + + Ok(drive_name.to_string()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument())] + pub fn get_disk_number(letter: char) -> Result { + let handle = file_handle(letter)?; + + + let mut bytes_ret = 0; + let mut vde = windows::Win32::System::Ioctl::VOLUME_DISK_EXTENTS::default(); + let buf_size = std::mem::size_of_val(&vde); + let res = unsafe { + DeviceIoControl( + handle.0, + IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, + None, + 0, + Some((&mut vde) as *mut _ as _), + buf_size as _, + Some(&mut bytes_ret), + None, + ) + }; + + // close it anyway: + drop(handle); + + res?; + + if vde.NumberOfDiskExtents == 1 { + debug!("found device volume {letter}."); + vde.Extents + .first() + .map(|vol| vol.DiskNumber) + .ok_or(Error::not_found()) + } else { + Err(Error::not_found()) + } + } + + + #[cfg_attr(feature = "tracing", tracing::instrument())] + pub fn unmount(letter: char) -> Result<(), Error> { + use windows::Win32::System::Ioctl::FSCTL_DISMOUNT_VOLUME; + + let h = winapi::file_handle(letter)?; + // let mut bytes_ret = 0; + // let mut overlapped = windows::Win32::System::IO::OVERLAPPED::default(); + trace!("Unmounting {letter} {:?} is valid: {}", h.0, !h.0.is_invalid()); + unsafe { + DeviceIoControl( + h.0, + FSCTL_DISMOUNT_VOLUME, + None, + 0, + None, + 0, + // Some(&mut bytes_ret), + None, + // Some((&mut overlapped) as *mut _), + None, + ) + }.map(|_| drop(h)) + .map_err(Error::from) + .or_else(|err| { + use windows::Win32::Storage::FileSystem::DeleteVolumeMountPointA; + + debug!("{err}, trying fallback method..."); + let (string, s) = winapi::pcstr_short(letter); + unsafe { DeleteVolumeMountPointA(s) }.map(|_| drop(string)) + .map_err(|err2| Error::chain(err2, [err])) + }) + } + + + struct FileHandle(pub HANDLE, Pin); + + impl Drop for FileHandle { + fn drop(&mut self) { + trace!("closing file handle {:?} by drop", self.0); + unsafe { windows::Win32::Foundation::CloseHandle(self.0) }.map_err(|err| error!("{err}")) + .ok(); + } + } + + + /// Format and produce `PCSTR` string pointing to the volume `letter` in short format, + /// e.g.: `D:`. + /// + /// Returned first element is a pinned `CString` that __must not__ be freed before the returned `PCSTR` is used. + #[inline] + pub fn pcstr_short(letter: char) -> (Pin, PCSTR) { pcstr(to_vol_path_short(letter)) } + + /// Format and produce `PCSTR` string pointing to the volume `letter` in short format, + /// e.g.: `\\?\D:`. + /// + /// Returned first element is a pinned `CString` that __must not__ be freed before the returned `PCSTR` is used. + #[inline] + pub fn pcstr_long(letter: char) -> (Pin, PCSTR) { pcstr(to_vol_path_long(letter)) } + + /// Format and produce `PCSTR` from the given string. + /// + /// Returned first element is a pinned `CString` that __must not__ be freed before the returned `PCSTR` is used. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn pcstr(s: String) -> (Pin, PCSTR) { + let s = Pin::new(CString::new(s).unwrap()); + let ps = PCSTR::from_raw(s.as_ptr() as _); + (s, ps) + } + + + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn get_vol_disk_number_winapi(letter: char) -> Result { + let handle = file_handle(letter)?; + + let mut bytes_ret = 0; + let mut vde = windows::Win32::System::Ioctl::VOLUME_DISK_EXTENTS::default(); + let buf_size = std::mem::size_of_val(&vde); + unsafe { + DeviceIoControl( + handle.0, + IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, + None, + 0, + Some((&mut vde) as *mut _ as _), + buf_size as _, + Some(&mut bytes_ret), + None, + ) + }?; + + // map with device address / DiskNumber: + let result = if vde.NumberOfDiskExtents == 1 { + debug!("found device volume {letter}."); + vde.Extents.first().map(|vol| vol.DiskNumber) + } else { + None + }; + + // close it anyway: + drop(handle); + + result.ok_or_else(|| { + use std::io::{Error, ErrorKind}; + let msg = format!("Cannot get volume {letter} disk number."); + Error::new(ErrorKind::NotFound, msg) + }) + } + + + #[cfg_attr(feature = "tracing", tracing::instrument())] + pub fn file_handle(letter: char) -> Result { + use windows::Win32::Storage::FileSystem::{FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES}; + use windows::Win32::Storage::FileSystem::CreateFileA; + + let (pinned, s) = pcstr_long(letter); + let h = unsafe { + CreateFileA( + s, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES(0), + HANDLE::default(), + ) + }?; + trace!("opened: {letter} handle, valid: {}", !h.is_invalid()); + Ok(FileHandle(h, pinned)) + } + + + #[cfg(test)] + mod tests { + use windows::core::s; + + + #[test] + fn pcstr_short() { + let (p, ps) = super::pcstr_short('A'); + let expected = s!("A:"); + let expected = unsafe { std::ffi::CStr::from_ptr(expected.0 as _) }; + let ps = unsafe { std::ffi::CStr::from_ptr(ps.0 as _) }; + assert_eq!(expected, ps); + drop(p); + } + + #[test] + fn pcstr_long_() { + let (p, ps) = super::pcstr_long('A'); + let expected = s!(r"\\.\A:"); + let expected = unsafe { std::ffi::CStr::from_ptr(expected.0 as _) }; + let ps = unsafe { std::ffi::CStr::from_ptr(ps.0 as _) }; + assert_eq!(expected, ps); + drop(p); + } + } +} + + +fn to_vol_path_short(letter: char) -> String { format!(r"{letter}:") } +fn to_vol_path_long(letter: char) -> String { format!(r"\\.\{letter}:") } + + +mod ps { + //! Powershell-related functions. + use crate::device::serial::SerialNumber; + // TODO: should be ExecutionError: + use crate::error::Error; + + + #[cfg_attr(feature = "tracing", tracing::instrument())] + pub fn vol_dn(letter: char) -> Result { + let mut cmd = vol_dn_cmd_1(letter); + let stdout = cmd.output().and_then(validate_output).or_else(|err| { + trace!("method #1: {err}"); + vol_dn_cmd_2(letter).output() + .and_then(validate_output) + })?; + std::str::from_utf8(&stdout)?.trim().parse().map_err(|err| { + use std::io::{Error, ErrorKind as Kind}; + Error::new(Kind::InvalidData, err).into() + }) + } + + #[cfg_attr(feature = "tracing", tracing::instrument())] + pub fn vol_sn(letter: char) -> Result { + let mut cmd = vol_sn_cmd_1(letter); + let stdout = cmd.output().and_then(validate_output).or_else(|err| { + trace!("method #1: {err}"); + vol_sn_cmd_2(letter).output() + .and_then(validate_output) + })?; + std::str::from_utf8(&stdout)?.trim().parse().map_err(Into::into) + } + + + /// Return stdout of the process output if not empty and no error occurred. + #[cfg_attr(feature = "tracing", tracing::instrument())] + fn validate_output(output: std::process::Output) -> Result, std::io::Error> { + use std::io::{Error, ErrorKind as Kind}; + + match output.status.exit_ok() { + Ok(_) => { + let stdout = std::str::from_utf8(&output.stdout).inspect_err(|err| error!("{err}")) + .ok() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .is_some(); + if stdout { + Ok(output.stdout) + } else { + Err(Error::new(Kind::InvalidData, "Empty output.")) + } + }, + Err(err) => { + let stderr = String::from_utf8_lossy(&output.stderr); + let stderr = stderr.trim(); + if !stderr.is_empty() { + Err(Error::new(Kind::Other, stderr)) + } else { + Err(Error::new(Kind::Other, err)) + } + }, + } + } + + + /// Retrieve __disk-number__ for the volume `letter`. + /// Perhaps not work on Win7 and earlier. + /// Returns exit code. + /// Returns number in the stdout, should be trimmed. + fn vol_dn_cmd_1(letter: char) -> std::process::Command { + let arg = + format!("Get-Partition -DriveLetter {letter} | Get-Disk | select-object -ExpandProperty DiskNumber"); + let mut cmd = std::process::Command::new("powershell"); + cmd.arg(arg); + cmd + } + + /// Retrieve __serial-number__ for the volume `letter`. + /// Perhaps not work on Win7 and earlier. + /// Returns exit code. + /// Returns sn-string in the stdout, should be trimmed. + fn vol_sn_cmd_1(letter: char) -> std::process::Command { + let arg = + format!("Get-Partition -DriveLetter {letter} | Get-Disk | select-object -ExpandProperty SerialNumber"); + let mut cmd = std::process::Command::new("powershell"); + cmd.arg(arg); + cmd + } + + /// Retrieve __disk-number__ for the volume `letter`. + /// Returns exit code always = `0`. + /// Returns sn-string in the stdout, should be trimmed. + /// If no result is found, returns empty stdout (after trim). + fn vol_dn_cmd_2(letter: char) -> std::process::Command { + let arg = format!("Get-CimInstance -ClassName Win32_DiskDrive | Get-CimAssociatedInstance -Association Win32_DiskDriveToDiskPartition | Get-CimAssociatedInstance -Association Win32_LogicalDiskToPartition | Where-Object DeviceId -eq '{letter}:' | Get-CimAssociatedInstance -Association Win32_LogicalDiskToPartition | Select-Object -ExpandProperty DiskIndex"); + let mut cmd = std::process::Command::new("powershell"); + cmd.arg(arg); + cmd + } + + /// Retrieve __serial-number__ for the volume `letter`. + /// Returns exit code always = `0`. + /// Returns sn-string in the stdout, should be trimmed. + /// If no result is found, returns empty stdout (after trim). + fn vol_sn_cmd_2(letter: char) -> std::process::Command { + let arg = format!("Get-CimInstance -ClassName Win32_DiskDrive | Get-CimAssociatedInstance -Association Win32_DiskDriveToDiskPartition | Get-CimAssociatedInstance -Association Win32_LogicalDiskToPartition | Where-Object DeviceId -eq '{letter}:' | Get-CimAssociatedInstance -Association Win32_LogicalDiskToPartition | Get-CimAssociatedInstance -Association Win32_DiskDriveToDiskPartition | Select-Object -ExpandProperty SerialNumber"); + let mut cmd = std::process::Command::new("powershell"); + cmd.arg(arg); + cmd + } +} diff --git a/support/device/src/retry.rs b/support/device/src/retry.rs new file mode 100644 index 00000000..b702d484 --- /dev/null +++ b/support/device/src/retry.rs @@ -0,0 +1,90 @@ +//! Retry utils + +use std::time::Duration; + + +#[derive(Clone)] +pub struct Retries { + /// How many iterations to perform before giving up. + pub iters: Iters, + /// Total awaiting time + pub total: Duration, +} + +impl Retries { + pub fn new(iters: Iters, total: Duration) -> Self { Self { iters, total } } +} + +impl Default for Retries where T: Default + IterTime { + fn default() -> Self { + Self { iters: Default::default(), + total: Duration::from_secs(10) } + } +} + +impl std::fmt::Display for Retries where T: std::fmt::Display + IterTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({} => {:?})", self.iters, self.total) + } +} +impl std::fmt::Debug for Retries where T: std::fmt::Debug + IterTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({:?} => {:?})", self.iters, self.total) + } +} + + +pub trait IterTime { + fn preferred_iter_time(&self) -> Duration; + + #[inline(always)] + fn interval(&self, total_wait: &Duration) -> Duration + where for<'t> &'t Self: IterTime { + calc_interval(total_wait, self) + } +} + + +impl IterTime for &'_ T { + #[inline(always)] + fn preferred_iter_time(&self) -> Duration { T::preferred_iter_time(*self) } + + #[inline(always)] + fn interval(&self, total_wait: &Duration) -> Duration + where for<'t> &'t Self: IterTime { + T::interval(*self, total_wait) + } +} + +pub fn calc_interval(wait: &Duration, cfg: T) -> Duration { + let iters = wait.as_millis() / cfg.preferred_iter_time().as_millis() as u128; + Duration::from_millis((wait.as_millis() / iters) as _) +} + + +#[derive(Clone, Default)] +pub struct DefaultIterTime; +const MIN_ITER_TIME: u64 = 100; + +impl IterTime for DefaultIterTime { + fn preferred_iter_time(&self) -> Duration { Duration::from_millis(MIN_ITER_TIME) } +} + +impl std::fmt::Display for DefaultIterTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}ms", MIN_ITER_TIME) } +} +impl std::fmt::Debug for DefaultIterTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Duration::from_millis(MIN_ITER_TIME).fmt(f) + } +} + + +impl IterTime for Duration { + fn preferred_iter_time(&self) -> Duration { self.clone() } + + fn interval(&self, total_wait: &Duration) -> Duration + where for<'t> &'t Self: IterTime { + calc_interval(total_wait, self) + } +} diff --git a/support/device/src/run.rs b/support/device/src/run.rs new file mode 100644 index 00000000..937b0f80 --- /dev/null +++ b/support/device/src/run.rs @@ -0,0 +1,80 @@ +use std::borrow::Cow; +use std::path::PathBuf; + +use futures::stream::FuturesUnordered; +use futures::{FutureExt, TryStreamExt}; +use futures_lite::StreamExt; + +use crate::device::query::Query; +use crate::device::wait_mode_data; +use crate::error::Error; +use crate::mount::UnmountAsync; +use crate::{install, device, usb, interface}; + + +type Result = std::result::Result; + + +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub async fn run(query: Query, + pdx: PathBuf, + no_install: bool, + no_read: bool, + force: bool) + -> Result> { + use crate::retry::{DefaultIterTime, Retries}; + let wait_data = Retries::::default(); + + + let to_run = if !no_install { + install::mount_and_install(query, &pdx, force).await? + .filter_map(|r| r.map_err(|e| error!("{e}")).ok()) + .flat_map(|path| { + async { + let (mount, path) = path.into_parts(); + mount.unmount().await?; + wait_mode_data(mount.device, wait_data.clone()).await + .map(|dev| { + (dev, path.into()) + }) + }.into_stream() + .filter_map(move |r| r.inspect_err(|e| error!("{e}")).ok()) + }) + .collect::)>>() + .await + } else { + usb::discover::devices_data()?.map(|dev| (dev, pdx.to_string_lossy())) + .collect() + }; + + + let mut to_read = Vec::with_capacity(to_run.len()); + let readers = FuturesUnordered::new(); + + for (mut device, path) in to_run { + use interface::r#async::Out; + + device.open()?; + { + let interface = device.interface()?; + interface.send_cmd(device::command::Command::Run { path: path.into_owned() }) + .await?; + } + + if !no_read { + to_read.push(device); + } + } + + if !no_read { + for device in to_read.iter_mut() { + readers.push(usb::io::redirect_to_stdout(device)); + } + } + + readers.inspect_err(|err| error!("{err}")) + .try_for_each_concurrent(8, |_| async { Ok(()) }) + .await?; + + Ok(to_read) +} diff --git a/support/device/src/send.rs b/support/device/src/send.rs new file mode 100644 index 00000000..5f59fe5b --- /dev/null +++ b/support/device/src/send.rs @@ -0,0 +1,67 @@ +use futures::{FutureExt, Stream, StreamExt, TryFutureExt}; +use futures_lite::stream; + +use crate::device::command::Command; +use crate::{device, usb, interface}; +use crate::error::Error; +use device::query::Query; +use interface::r#async::Out; + + +type Result = std::result::Result; + + +/// Fails if can't map specified port to device in case of query is a port name/path. +/// Use [[send_to_interfaces]] instead if device mapping not needed. +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn send_to_devs(query: Query, + cmd: Command, + read: bool) + -> Result>> { + let devices = usb::discover::devices_data_for(query).await?; + + if devices.is_empty() { + return Err(Error::not_found()); + } + + let devices = devices.into_iter().flat_map(|mut dev| { + dev.open().inspect_err(|err| error!("{err}")).ok()?; + Some(dev) + }); + let stream = stream::iter(devices).flat_map_unordered(None, move |mut dev| { + let cmd = cmd.clone(); + async move { + match dev.interface_mut().inspect_err(|err| error!("{err}")) { + Ok(interface) => { + if read { + interface.send_cmd(cmd).await?; + usb::io::redirect_interface_to_stdout(interface).await?; + } else { + interface.send_cmd(cmd).await?; + } + Ok(()) + }, + Err(err) => Err(err), + }?; + Ok::<_, Error>(dev) + }.into_stream() + .boxed_local() + }); + Ok(stream) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn send_to_interfaces(query: Query, + cmd: Command) + -> Result>> { + usb::discover::for_each_data_interface(query, move |interface| { + let cmd = cmd.clone(); + async move { + interface.send_cmd(cmd.clone()) + .inspect_err(|err| error!("{err}")) + .await?; + Ok::<_, Error>(interface) + } + }).await +} diff --git a/support/device/src/serial/async.rs b/support/device/src/serial/async.rs new file mode 100644 index 00000000..acea0c51 --- /dev/null +++ b/support/device/src/serial/async.rs @@ -0,0 +1,29 @@ +#![cfg(feature = "tokio-serial")] +#![cfg(feature = "tokio")] + +use std::ops::DerefMut; + +use tokio::io::AsyncWriteExt; + +use crate::error::Error; +use super::Interface; + + +impl crate::interface::r#async::Out for Interface { + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] + async fn send(&self, data: &[u8]) -> Result { + trace!("writing {} bytes to {}", data.len(), self.info.port_name); + if let Some(ref port) = self.port { + let mut port = port.try_borrow_mut()?; + let port = port.deref_mut(); + port.write_all(data).await?; + port.flush().await?; + Ok(data.len()) + } else { + Err(Error::not_ready()) + } + } +} + + +impl crate::interface::r#async::In for Interface {} diff --git a/support/device/src/serial/blocking.rs b/support/device/src/serial/blocking.rs new file mode 100644 index 00000000..08bcfac5 --- /dev/null +++ b/support/device/src/serial/blocking.rs @@ -0,0 +1,23 @@ +use std::io::prelude::*; + +use crate::error::Error; +use super::Interface; + + +impl crate::interface::blocking::Out for Interface { + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] + fn send_cmd(&self, cmd: crate::device::command::Command) -> Result { + trace!("sending `{cmd}` to {}", self.info.port_name); + if let Some(ref port) = self.port { + let s = cmd.with_break(); + let mut port = port.try_borrow_mut()?; + port.write_all(s.as_bytes())?; + port.flush()?; + Ok(s.as_bytes().len()) + } else { + Err(Error::not_ready()) + } + } +} + +impl crate::interface::blocking::In for Interface {} diff --git a/support/device/src/serial/discover.rs b/support/device/src/serial/discover.rs new file mode 100644 index 00000000..1c67f369 --- /dev/null +++ b/support/device/src/serial/discover.rs @@ -0,0 +1,115 @@ +use std::borrow::Cow; +use std::fmt::Debug; + +use serialport::{SerialPortInfo, SerialPortType, available_ports}; + +use crate::device::Device; +use crate::error::Error; +use crate::{VENDOR_ID, PRODUCT_ID_DATA}; + + +/// Enumerate all serial ports on the system for Playdate devices. +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub fn ports() -> Result, Error> { + let iter = available_ports()?.into_iter().filter(|port| { + match port.port_type { + SerialPortType::UsbPort(ref info) => { + info.vid == VENDOR_ID && info.pid == PRODUCT_ID_DATA + }, + _ => false, + } + }); + Ok(iter) +} + + +/// Search exact one serial port for device with same serial number. +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub fn port(sn: &SN) -> Result + where SN: PartialEq + Debug { + let port = ports()?.find(move |port| { + match port.port_type { + SerialPortType::UsbPort(ref info) => { + info.serial_number.as_ref().filter(|s| sn.eq(s)).is_some() + }, + _ => false, + } + }); + // TODO: error: serial not found for sn + port.ok_or_else(|| Error::not_found()) +} + + +/// Search serial ports for device with same serial number, +/// or __any__ Playdate- serial port if `sn` is `None`. +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub fn ports_with(sn: Option) -> Result, Error> + where SN: PartialEq + Debug { + Ok(ports()?.filter(move |port| sn.as_ref().map(|sn| port_sn_matches(port, sn)).unwrap_or(true))) +} + + +/// Search serial ports for device with same serial number, +/// or __any__ Playdate- serial port if `sn` is `None`. +/// +/// In case of just one device and just one port found, serial number will not be used for matching, so it returns. +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub fn ports_with_or_single(sn: Option) -> Result, Error> + where SN: PartialEq + Debug { + let ports: Vec<_> = ports()?.collect(); + let devs: Vec<_> = crate::usb::discover::devices_data()?.collect(); + + if ports.len() == 1 && devs.len() == 1 { + trace!("Auto-match single found dev with port without sn match."); + let psn = match &ports[0].port_type { + SerialPortType::UsbPort(usb) => usb.serial_number.as_deref(), + SerialPortType::PciPort => None, + SerialPortType::BluetoothPort => None, + SerialPortType::Unknown => None, + }; + let name = &ports[0].port_name; + trace!("Found serial port: {name}, sn: {psn:?} for dev: {sn:?}",); + Ok(ports) + } else { + let ports = ports.into_iter() + .filter(move |port| sn.as_ref().map(|sn| port_sn_matches(port, sn)).unwrap_or(true)); + Ok(ports.collect()) + } +} + + +fn port_sn_matches(port: &SerialPortInfo, sn: &SN) -> bool + where SN: PartialEq + Debug { + match port.port_type { + SerialPortType::UsbPort(ref info) => { + trace!("found port: {}, dev sn: {:?}", port.port_name, info.serial_number); + info.serial_number + .as_deref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .filter(|s| { + let res = sn.eq(s); + trace!("sn is ≈ {res}"); + res + }) + .is_some() + }, + _ => false, + } +} + + +#[cfg_attr(feature = "tracing", tracing::instrument(skip(dev)))] +/// Search serial ports for `device`` with same serial number. +#[cfg(not(target_os = "windows"))] +pub fn ports_for(dev: &Device) -> Result + '_, Error> { + ports_with(dev.info().serial_number().map(Cow::Borrowed)) +} +#[cfg(target_os = "windows")] +/// +/// _On Windows in some strange cases of serial number of the device that behind founded COM port +/// can't be determined of we get just part of it, so we need to use another method to match devices +/// in case of there is just one device and port._ +pub fn ports_for(dev: &Device) -> Result + '_, Error> { + ports_with_or_single(dev.info().serial_number().map(Cow::Borrowed)).map(|v| v.into_iter()) +} diff --git a/support/device/src/serial/methods.rs b/support/device/src/serial/methods.rs new file mode 100644 index 00000000..637fd84b --- /dev/null +++ b/support/device/src/serial/methods.rs @@ -0,0 +1,108 @@ +use std::borrow::Cow; + +use crate::device::serial::SerialNumber; +use crate::device::Device; +use crate::error::Error; +use crate::usb; + + +type Result = std::result::Result; + + +/// Create `Device` with serial interface by given `port` name/path. +#[cfg_attr(feature = "tracing", tracing::instrument(fields(port = port.as_ref())))] +pub async fn dev_with_port>(port: S) -> Result { + use serialport::SerialPort; + + let name = port.as_ref(); + let port = super::open(name)?; + + let dev = port.as_ref() + .name() + .map(Cow::from) + .or(Some(name.into())) + .and_then(|name| { + let mut dev = SerialNumber::try_from(name.as_ref()).map_err(Error::from) + .and_then(|sn| usb::discover::device(&sn)) + .ok()?; + let mut inter = super::Interface::new(unknown_serial_port_info(name)); + inter.set_port(port); + dev.set_interface(crate::interface::Interface::Serial(inter)); + Some(dev) + }); + + // TODO: error: device not found for serial port + dev.ok_or_else(|| Error::not_found()) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub fn unknown_serial_port_info(port_name: Cow<'_, str>) -> serialport::SerialPortInfo { + let unknown = serialport::SerialPortType::Unknown; + serialport::SerialPortInfo { port_name: port_name.to_string(), + port_type: unknown } +} + + +/// Open given `interface` and read to stdout infinitely. +#[cfg(feature = "tokio-serial")] +#[cfg_attr(feature = "tracing", tracing::instrument(skip(interface), fields(interface.port_name = interface.info().port_name)))] +pub async fn redirect_interface_to_stdout(interface: &super::Interface) -> Result<(), Error> { + let mut out = tokio::io::stdout(); + let mut port = interface.port + .as_ref() + .ok_or(Error::not_ready())? + .try_borrow_mut()?; + tokio::io::copy(port.as_mut(), &mut out).await + .map_err(Error::from) + .map(|bytes| debug!("Redirected {bytes} bytes to stdout.")) +} + +/// Open given `port` and read to stdout infinitely. +#[cfg(feature = "tokio-serial")] +#[cfg_attr(feature = "tracing", tracing::instrument(fields(port.name = serialport::SerialPort::name(port.as_ref()))))] +pub async fn redirect_port_to_stdout(port: &mut super::Port) -> Result<(), Error> { + let mut out = tokio::io::stdout(); + tokio::io::copy(port, &mut out).await + .map_err(Error::from) + .map(|bytes| debug!("Redirected {bytes} bytes to stdout.")) +} + + +/// Open port by given `port` name/path and read to stdout infinitely. +#[cfg(feature = "tokio-serial")] +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub async fn redirect_to_stdout(port: S) -> Result<(), Error> + where S: for<'a> Into> + std::fmt::Debug { + let mut port = super::open(port)?; + redirect_port_to_stdout(&mut port).await +} + +/// Open port by given `port` name/path and read to stdout infinitely. +#[cfg_attr(feature = "tracing", tracing::instrument())] +pub fn redirect_to_stdout_blocking(port: S) -> Result<(), Error> + where S: for<'a> Into> + std::fmt::Debug { + let mut port = super::open(port)?; + + #[cfg(feature = "tokio-serial")] + { + let handle = tokio::runtime::Handle::current(); + std::thread::spawn(move || { + let fut = redirect_port_to_stdout(&mut port); + let res = handle.block_on(fut); + if let Err(err) = res { + error!("Error redirecting to stdout: {err}"); + } + }).join() + .expect("Error when join on the redirecting to stdout thread."); + + Ok(()) + } + #[cfg(not(feature = "tokio-serial"))] + { + let mut port = super::open(port)?; + let mut out = std::io::stdout(); + std::io::copy(&mut port, &mut out).map_err(Error::from) + .map(|bytes| debug!("Redirected {bytes} bytes to stdout.")) + } +} diff --git a/support/device/src/serial/mod.rs b/support/device/src/serial/mod.rs new file mode 100644 index 00000000..1a409d78 --- /dev/null +++ b/support/device/src/serial/mod.rs @@ -0,0 +1,131 @@ +use std::borrow::Cow; +use std::cell::RefCell; + +use crate::error::Error; + + +pub mod discover; +mod blocking; +mod r#async; + +mod methods; +pub use methods::*; + + +#[cfg(not(feature = "tokio-serial"))] +type Port = Box; +#[cfg(feature = "tokio-serial")] +type Port = Box; + +pub struct Interface { + info: serialport::SerialPortInfo, + port: Option>, +} + + +impl std::fmt::Display for Interface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use serialport::SerialPort; + + let port_name = &self.info.port_name; + let name = self.port + .as_ref() + .map(|p| { + p.try_borrow() + .ok() + .map(|p| p.name().filter(|s| s != port_name)) + .flatten() + }) + .flatten(); + + write!(f, "serial:{}", name.as_deref().unwrap_or(port_name)) + } +} + +impl std::fmt::Debug for Interface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Interface") + .field("name", &self.info.port_name) + .field("opened", &self.port.is_some()) + .finish() + } +} + + +impl Interface { + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn new(info: serialport::SerialPortInfo) -> Self { Self { info, port: None } } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn new_with(port: Port, name: Option) -> Self { + use serialport::{SerialPort, SerialPortType, SerialPortInfo}; + + let name = port.name().or(name).map(Cow::from).unwrap_or_default(); + let info = SerialPortInfo { port_name: name.to_string(), + port_type: SerialPortType::Unknown }; + + let mut result = Self::new(info); + result.set_port(port); + result + } + + pub fn info(&self) -> &serialport::SerialPortInfo { &self.info } + pub fn is_open(&self) -> bool { self.port.is_some() } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn set_port(&mut self, port: Port) { self.port = Some(RefCell::new(port)); } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn open(&mut self) -> Result<(), Error> { + if self.port.is_some() { + Ok(()) + } else { + let port = open(&self.info.port_name).map(RefCell::new)?; + self.port = Some(port); + Ok(()) + } + } + + + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn close(&mut self) { self.port.take(); } +} + + +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub fn open<'a, S: Into>>(port_name: S) -> Result + where S: std::fmt::Debug { + trace!("opening port {port_name:?}"); + let builder = port_builder(port_name); + + let port; + #[cfg(not(feature = "tokio-serial"))] + { + port = builder.open()?; + } + #[cfg(feature = "tokio-serial")] + { + use tokio_serial::SerialPortBuilderExt; + port = builder.open_native_async().map(Box::new)?; + } + + { + use serialport::SerialPort; + let name = port.as_ref().name(); + let name = name.as_deref().unwrap_or("n/a"); + trace!("opened port: {name}"); + } + Ok(port) +} + +fn port_builder<'a>(port_name: impl Into>) -> serialport::SerialPortBuilder { + serialport::new(port_name, 115200).data_bits(serialport::DataBits::Eight) +} + + +/* NOTE: This can be safely sent between thread, but not inner port, + but that's okay because it's boxen under `RefCell`. + Probably should be pinned, but not sure yet. +*/ +unsafe impl Send for Interface {} +unsafe impl Sync for Interface {} diff --git a/support/device/src/usb/discover.rs b/support/device/src/usb/discover.rs new file mode 100644 index 00000000..1b2f949f --- /dev/null +++ b/support/device/src/usb/discover.rs @@ -0,0 +1,179 @@ +#[cfg(feature = "futures")] +use futures::{Stream, StreamExt}; + +use crate::device::query; +use crate::error::Error; +use crate::device::serial::SerialNumber as Sn; +use crate::{usb, serial, interface}; +use crate::PRODUCT_ID_DATA; +use crate::PRODUCT_ID_STORAGE; +use crate::VENDOR_ID; + +use super::Device; + +type Result = std::result::Result; + + +/// Enumerate all Playdate- devices. +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub fn devices() -> Result> { + Ok(nusb::list_devices()?.filter(|d| d.vendor_id() == VENDOR_ID) + .map(|info| Device::new(info))) +} + +/// Search Playdate- devices that in data (serial/modem/telnet) mode. +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub fn devices_data() -> Result> { + devices().map(|iter| iter.filter(|d| d.info.product_id() == PRODUCT_ID_DATA)) +} + +/// Search Playdate- devices that in storage (data-disk) mode. +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub fn devices_storage() -> Result> { + devices().map(|iter| iter.filter(|d| d.info.product_id() == PRODUCT_ID_STORAGE)) +} + +/// Search exact one device with same serial number. +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub fn device(sn: &Sn) -> Result { + devices()?.find(|d| d.info.serial_number().filter(|s| sn.eq(s)).is_some()) + .ok_or_else(|| Error::not_found()) +} + +/// Search devices with same serial number, +/// or __any__ Playdate- device if `sn` is `None`. +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub fn devices_with(sn: Option) -> Result> { + Ok(devices()?.filter(move |dev| { + if let Some(sn) = sn.as_ref() { + dev.info().serial_number().filter(|s| sn.eq(s)).is_some() + } else { + true + } + })) +} + +/// Search devices with same serial number in data mode, +/// or __any__ Playdate- device if `sn` is `None`. +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub fn devices_data_with(sn: Option) -> Result> { + Ok(devices_data()?.filter(move |dev| { + if let Some(sn) = sn.as_ref() { + dev.info().serial_number().filter(|s| sn.eq(s)).is_some() + } else { + true + } + })) +} + + +#[cfg(feature = "futures")] +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub async fn devices_data_for(query: query::Query) -> Result> { + use query::Value as Query; + use serial::dev_with_port; + + + let try_by_port = |port_pref: String| { + async { + let existing = serial::discover::ports().map(|ports| { + ports.into_iter() + .find(|p| p.port_name == port_pref) + .map(serial::Interface::new) + }); + match existing { + Ok(Some(port)) => { + if let serialport::SerialPortType::UsbPort(serialport::UsbPortInfo { serial_number: + Some(ref sn), + .. }) = port.info().port_type + { + let name = port.info().port_name.as_str().to_owned(); + Sn::try_from(sn.as_str()).map_err(Error::from) + .and_then(|sn| usb::discover::devices_data_with(Some(sn))) + .map(|mut devs| devs.next()) + .map(move |mb| { + mb.map(|mut dev| { + dev.set_interface(interface::Interface::Serial(port)); + dev + }) + }) + .map_err(|err| { + error!("Unable to map specified port {name} to device: {err}"); + Error::chain(Error::not_found(), [err]) + }) + .ok() + .flatten() + .ok_or_else(Error::not_found) + } else { + dev_with_port(port_pref).await + } + }, + Ok(None) => dev_with_port(port_pref).await, + Err(err) => { + dev_with_port(port_pref).await + .map_err(|err2| Error::chain(err2, [err])) + }, + } + } + }; + + + let devs = match query.value { + Some(Query::Path(port)) => { + vec![try_by_port(port.to_string_lossy().to_string()).await?] + }, + Some(Query::Com(port)) => vec![try_by_port(format!("COM{port}")).await?], + Some(Query::Serial(sn)) => devices_data_with(Some(sn)).map(|i| i.collect())?, + None => devices_data_with(None).map(|i| i.collect())?, + }; + + Ok(devs) +} + + +#[cfg(feature = "futures")] +#[cfg_attr(feature = "tracing", tracing::instrument(skip(f)))] +pub async fn for_each_data_interface(query: query::Query, mut f: F) -> Result> + where Fut: std::future::Future, + F: FnMut(interface::Interface) -> Fut { + use query::Value as Query; + use serial::unknown_serial_port_info; + + + let devs = match query.value { + Some(Query::Path(port)) => { + let name = port.to_string_lossy(); + let mut interface = serial::Interface::new(unknown_serial_port_info(name)); + interface.open()?; + let interface = interface::Interface::Serial(interface); + futures_lite::stream::once(f(interface).await).left_stream() + }, + Some(Query::Com(port)) => { + let name = format!("COM{port}").into(); + let mut interface = serial::Interface::new(unknown_serial_port_info(name)); + interface.open()?; + let interface = interface::Interface::Serial(interface); + futures_lite::stream::once(f(interface).await).left_stream() + }, + Some(Query::Serial(sn)) => { + let mut interfaces = Vec::new(); + for mut dev in devices_data_with(Some(sn))? { + dev.open()?; + dev.interface()?; + interfaces.push(f(dev.interface.take().unwrap()).await); + } + futures_lite::stream::iter(interfaces).right_stream() + }, + None => { + let mut interfaces = Vec::new(); + for mut dev in devices_data_with(None)? { + dev.open()?; + dev.interface()?; + interfaces.push(f(dev.interface.take().unwrap()).await); + } + futures_lite::stream::iter(interfaces).right_stream() + }, + }; + + Ok(devs) +} diff --git a/support/device/src/usb/io.rs b/support/device/src/usb/io.rs new file mode 100644 index 00000000..dee31828 --- /dev/null +++ b/support/device/src/usb/io.rs @@ -0,0 +1,146 @@ +use std::io::Write; + +use futures_lite::future::block_on; + + +use futures_lite::StreamExt; +use nusb::transfer::RequestBuffer; +use nusb::{DeviceInfo, Interface}; + +use crate::device::Device; +use crate::error::Error; +use crate::serial::redirect_interface_to_stdout as redirect_serial_to_stdout; +use crate::usb::mode::DeviceMode; +use crate::usb::mode::Mode; +use crate::usb::BULK_IN; + + +#[cfg_attr(feature = "tracing", tracing::instrument(skip(interface)))] +pub fn read_interface(interface: &Interface, + buf_size: usize, + bufs: usize) + -> Result>, Error> { + let mut inp = interface.bulk_in_queue(BULK_IN); + + // preallocate buffers + while inp.pending() < bufs { + inp.submit(RequestBuffer::new(buf_size)); + } + + let stream = futures_lite::stream::poll_fn(move |ctx| { + inp.poll_next(ctx) + .map(|out| -> Result<_, Error> { + let data = out.into_result()?; + let s = std::str::from_utf8(&data)?.to_owned(); + inp.submit(RequestBuffer::reuse(data, buf_size)); + Ok(s) + }) + .map(|out| Some(out)) + }); + + Ok(stream) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(skip(interface, map)))] +pub fn read_while_map(interface: &Interface, + buf_size: usize, + buffers: usize, + mut map: F) + -> Result, Error> + where F: FnMut(&[u8]) -> Option +{ + let mut inp = interface.bulk_in_queue(BULK_IN); + + // preallocate buffers + while inp.pending() < buffers { + inp.submit(RequestBuffer::new(buf_size)); + } + + let stream = futures_lite::stream::poll_fn(move |ctx| { + inp.poll_next(ctx).map(|out| -> Option<_> { + match out.into_result() { + Ok(data) => { + let res = map(data.as_slice()); + if res.is_some() { + inp.submit(RequestBuffer::reuse(data, buf_size)); + } else { + trace!("cancel all IN queue, by predicate."); + inp.cancel_all(); + } + res + }, + Err(err) => { + trace!("cancel all IN queue, by err: {err}."); + inp.cancel_all(); + None + }, + } + }) + }); + + Ok(stream) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub fn read_once(device: DeviceInfo) -> Result<(String, Interface), Error> { + let mode = device.mode(); + if !matches!(mode, Mode::Data) { + return Err(Error::WrongState(mode)); + } + + + let device = device.open()?; + let inter = device.claim_interface(1)?; + + let stream = read_while_map(&inter, 256, 2, |data| { + match std::str::from_utf8(&data) { + Ok(s) => { + if s.trim().is_empty() { + None + } else { + Some(s.to_owned()) + } + }, + Err(err) => { + error!("{err:?}"); + None + }, + } + })?.fold(String::new(), |acc, ref s| acc + s); + let s = block_on(stream); + Ok((s, inter)) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub async fn redirect_to_stdout(device: &mut Device) -> Result<(), Error> { + let mode = device.mode(); + if !matches!(mode, Mode::Data) { + return Err(Error::WrongState(mode)); + } + + device.open()?; + redirect_interface_to_stdout(device.interface_mut()?).await?; + + Ok(()) +} + +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub async fn redirect_interface_to_stdout(interface: &mut crate::interface::Interface) -> Result<(), Error> { + match interface { + crate::interface::Interface::Usb(interface) => { + let mut stdout = std::io::stdout(); + let to_stdout = move |data: &[u8]| stdout.write_all(data).inspect_err(|err| error!("{err}")).ok(); + let stream = read_while_map(&interface.inner, 256, 2, to_stdout)?; + if let Some(_) = stream.last().await { + trace!("Read stream complete."); + } + }, + crate::interface::Interface::Serial(interface) => { + interface.open()?; + redirect_serial_to_stdout(interface).await?; + }, + } + Ok(()) +} diff --git a/support/device/src/usb/mod.rs b/support/device/src/usb/mod.rs new file mode 100644 index 00000000..ece50d64 --- /dev/null +++ b/support/device/src/usb/mod.rs @@ -0,0 +1,340 @@ +use std::borrow::Cow; + +use std::pin::Pin; +use std::task::Context; +use std::task::Poll; + +use futures::FutureExt; +use futures::TryFutureExt; +use nusb::transfer::RequestBuffer; +use nusb::transfer::TransferError; +use nusb::DeviceInfo; +use nusb::InterfaceInfo; +use object_pool::Pool; +use object_pool::Reusable; + +use crate::device::command::Command; +use crate::device::Device; +use crate::error::Error; + +use self::mode::DeviceMode; +use self::mode::Mode; + +pub mod mode; +pub mod discover; +pub mod io; + + +const BULK_IN: u8 = 0x81; +const BULK_OUT: u8 = 0x01; + +#[allow(dead_code)] +const INTERRUPT_IN: u8 = 0x82; + + +pub trait HaveDataInterface { + fn data_interface_number(&self) -> Option; + fn have_data_interface(&self) -> bool { self.data_interface_number().is_some() } +} + +impl HaveDataInterface for DeviceInfo { + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn data_interface_number(&self) -> Option { + self.interfaces() + .find(|i| i.class() == 0xA | 2) + .map(|i| i.interface_number()) + } +} + +impl HaveDataInterface for nusb::Device { + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] + fn data_interface_number(&self) -> Option { + let cfg = self.active_configuration().ok()?; + for i in cfg.interfaces() { + let bulk = i.alt_settings().find(|i| i.class() == 0xA | 2); + if bulk.is_some() { + return bulk.map(|i| i.interface_number()); + } + } + None + } +} + +impl HaveDataInterface for Device { + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn data_interface_number(&self) -> Option { + self.info + .data_interface_number() + .or_else(|| self.inner.as_ref()?.data_interface_number()) + } +} + +pub trait MassStorageInterface { + fn storage_interface(&self) -> Option<&InterfaceInfo>; + fn have_storage_interface(&self) -> bool; +} + +impl MassStorageInterface for DeviceInfo { + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn storage_interface(&self) -> Option<&InterfaceInfo> { self.interfaces().find(|i| i.class() == 8) } + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn have_storage_interface(&self) -> bool { self.storage_interface().is_some() } +} + + +impl Device { + /// 1. Find this device + /// 1. Compare `mode` of `this` vs. just found + /// 1. [if changed] Update state of `this`, drop all pending transfers if needed + /// to prevent future errors when send to unexisting interface. + /// 1. Return `true` if `mode` changed. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn refresh(&mut self) -> Result { + let mode = self.info.mode(); + if mode != self.mode { + self.mode = mode; + self.interface.take(); + self.inner.take(); + debug!( + "{}: refreshed by existing.", + self.info.serial_number().unwrap_or("unknown") + ); + Ok(true) + } else { + let updated = crate::usb::discover::devices()?.find(|dev| { + let serial = dev.info().serial_number(); + serial.is_some() && serial == self.info.serial_number() + }); + if let Some(dev) = updated { + let mode = dev.mode_cached(); + let changed = mode != self.mode; + if changed { + self.mode = mode; + self.info = dev.info; + self.interface.take(); + self.inner.take(); + debug!( + "{}: refreshed by existing new.", + self.info.serial_number().unwrap_or("unknown") + ); + } + Ok(changed) + } else { + debug!( + "{}: device not found.", + self.info.serial_number().unwrap_or("unknown") + ); + self.interface.take(); + self.inner.take(); + Ok(true) + } + } + } + + + /// Open USB interface if available, + /// otherwise try open serial port if available. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn open(&mut self) -> Result<(), Error> { + if !matches!(self.mode, Mode::Data) { + return Err(Error::WrongState(self.mode)); + } + + trace!("opening device"); + + // Special case: if we already have an interface, mostly possible serial: + if self.interface.is_some() { + return match self.interface_mut()? { + crate::interface::Interface::Serial(i) => i.open(), + _ => Ok(()), + }; + } + + if self.have_data_interface() { + let bulk = self.try_bulk().map(|_| {}); + if let Some(err) = bulk.err() { + self.try_serial().map_err(|err2| Error::chain(err2, [err])) + } else { + self.interface() + } + } else { + self.try_serial() + }?; + Ok(()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] + fn try_bulk(&mut self) -> Result<&crate::interface::Interface, Error> { + if let Some(ref io) = self.interface { + Ok(io) + } else if let Some(ref dev) = self.inner { + let id = self.info + .data_interface_number() + .or_else(|| dev.data_interface_number()) + .ok_or_else(|| Error::not_found())?; + // previously used 0x01. + self.interface = Some(Interface::from(dev.claim_interface(id)?).into()); + Ok(self.interface.as_ref().unwrap()) + } else { + let dev = self.info.open()?; + let id = self.info + .data_interface_number() + .or_else(|| dev.data_interface_number()) + .ok_or_else(|| Error::not_found())?; + self.interface = Some(Interface::from(dev.claim_interface(id)?).into()); + self.inner = Some(dev); + Ok(self.interface.as_ref().unwrap()) + } + } + + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] + fn try_serial(&mut self) -> Result<&crate::interface::Interface, Error> { + use crate::serial::Interface; + + + let mut errors = Vec::new(); + let port = { + crate::serial::discover::ports_for(&self).map(|ports| ports.map(|port| Interface::new(port)))? + .find_map(|mut port| { + // try to open port, we could get an permission error + match port.open() { + Ok(_) => Some(port), + Err(err) => { + errors.push(err); + None + }, + } + }) + }; + + if let Some(port) = port { + self.interface = Some(port.into()); + self.interface() + } else { + Err(Error::chain(Error::not_found(), errors)) + } + } + + + /// Async read-write interface. + pub fn interface(&self) -> Result<&crate::interface::Interface, Error> { + self.interface.as_ref().ok_or_else(|| Error::not_ready()) + } + + pub fn interface_mut(&mut self) -> Result<&mut crate::interface::Interface, Error> { + self.interface.as_mut().ok_or_else(|| Error::not_ready()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] + pub fn set_interface(&mut self, interface: crate::interface::Interface) { + self.close(); + self.interface = Some(interface); + } + + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] + pub fn close(&mut self) { + self.info.serial_number().map(|s| debug!("closing {s}")); + self.interface.take(); + self.inner.take(); + } + + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] + pub fn close_with_reset(&mut self) { + self.info.serial_number().map(|s| debug!("closing* {s}")); + self.interface.take(); + if let Some(dev) = self.inner.take() { + dev.reset().map_err(|err| error!("{err}")).ok(); + } + } +} + +impl crate::interface::blocking::Out for Interface { + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn send_cmd(&self, cmd: Command) -> Result { + use crate::interface::r#async; + let fut = ::send_cmd(self, cmd); + futures_lite::future::block_on(fut).map_err(Into::into) + } +} + +impl crate::interface::blocking::In for Interface {} + + +impl crate::interface::r#async::Out for Interface { + #[cfg_attr(feature = "tracing", tracing::instrument)] + async fn send(&self, data: &[u8]) -> Result { self.write(data).map_err(Into::into).await } +} + +impl crate::interface::r#async::In for Interface {} + + +pub struct Interface { + inner: nusb::Interface, +} + +impl From for Interface { + fn from(interface: nusb::Interface) -> Self { Self { inner: interface } } +} + +impl std::fmt::Display for Interface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "usb") } +} + +impl std::fmt::Debug for Interface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "usb") } +} + +impl Interface { + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn write(&self, data: &[u8]) -> impl std::future::Future> { + trace!("writing {} bytes", data.len()); + self.inner.bulk_out(BULK_OUT, data.to_vec()).map(|comp| { + // TODO: attach data to the pool + let written = comp.data.actual_length(); + let data = comp.data.reuse(); + let s = std::str::from_utf8(&data).map(Cow::Borrowed) + .unwrap_or_else(|_| { + Cow::Owned(hex::encode_upper(&data)) + }); + trace!("sent, resp: ({written}) '{s}'"); + comp.status.map(|_| written) + }) + } +} + + +pub struct PoolStream<'pool> { + pool: &'pool Pool>, + queue: nusb::transfer::Queue, + buffer_size: usize, + // inner: futures_lite::stream::PollFn, TransferError>>>, +} + +impl<'pool> futures::Stream for PoolStream<'pool> { + type Item = Result>, TransferError>; + + fn poll_next(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + self.queue.poll_next(ctx).map(|comp| { + let data = comp.data; + match comp.status { + Ok(_) => { + // prepare next request + let buffer_size = self.buffer_size; + let (_, buf) = + self.pool.pull(|| Vec::with_capacity(buffer_size)).detach(); + self.queue.submit(RequestBuffer::reuse(buf, buffer_size)); + // make received data reusable + let data = Reusable::new(self.pool, data); + Some(Ok(data)) + }, + Err(err) => { + self.pool.attach(data); + self.queue.cancel_all(); + Some(Err(err)) + }, + } + }) + } + + fn size_hint(&self) -> (usize, Option) { (0, Some(self.queue.pending())) } +} diff --git a/support/device/src/usb/mode.rs b/support/device/src/usb/mode.rs new file mode 100644 index 00000000..ff4614df --- /dev/null +++ b/support/device/src/usb/mode.rs @@ -0,0 +1,46 @@ +use nusb::DeviceInfo; + +use crate::PRODUCT_ID_DATA; +use crate::PRODUCT_ID_STORAGE; + + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Mode { + /// DATA / COMM + Data, + /// MASS_STORAGE + Storage, + Unknown, +} + +impl std::fmt::Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + Mode::Data => 'D', + Mode::Storage => 'S', + Mode::Unknown => '?', + }) + } +} + + +pub trait DeviceMode { + /// USB device mode determined by the product ID. + fn mode(&self) -> Mode; +} + + +impl DeviceMode for DeviceInfo { + fn mode(&self) -> Mode { + match self.product_id() { + PRODUCT_ID_DATA => Mode::Data, + PRODUCT_ID_STORAGE => Mode::Storage, + _ => Mode::Unknown, + } + } +} + + +impl DeviceMode for super::Device { + fn mode(&self) -> Mode { self.info().mode() } +} diff --git a/support/sim-ctrl/Cargo.toml b/support/sim-ctrl/Cargo.toml new file mode 100644 index 00000000..273eb49a --- /dev/null +++ b/support/sim-ctrl/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "playdate-simulator-utils" +version = "0.1.0" +readme = "README.md" +description = "Cross-platform utils to deal with Playdate Simulator." +keywords = ["playdate", "sdk", "utils"] +categories = ["development-tools"] +edition.workspace = true +license.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true + + +[dependencies] +thiserror.workspace = true +log.workspace = true +tracing = { version = "0.1", optional = true } + +[dependencies.utils] +workspace = true +default-features = false + +[dependencies.tokio] +features = ["process"] +default-features = false +workspace = true +optional = true diff --git a/support/sim-ctrl/README.md b/support/sim-ctrl/README.md new file mode 100644 index 00000000..2843f61f --- /dev/null +++ b/support/sim-ctrl/README.md @@ -0,0 +1,44 @@ +# Playdate Simulator Utils + +Cross-platform utils to do things with Playdate Simulator. + + +Usage: + +```rust +let pdx = PathBuf::from("path/to/my-game.pdx"); +let sdk = PathBuf::from("path/to/playdate-sdk"); + +// Create a future with command execution: +simulator::run::run(&pdx, Some(&sdk)).await; + +// Or create a command and do whatever: +let mut cmd = simulator::run::command(&pdx, Some(&sdk)).unwrap(); +let stdout = cmd.output().unwrap().stdout; +println!("Sim output: {}", std::str::from_utf8(&stdout).unwrap()); +``` + + +## Prerequisites + +1. Rust __nightly__ toolchain +3. [Playdate SDK][sdk] with Simulator + - Ensure that env var `PLAYDATE_SDK_PATH` points to the SDK root. _This is optional, but good move to help the tool to find SDK, and also useful if you have more then one version of SDK._ + + +[playdate-website]: https://play.date +[sdk]: https://play.date/dev/#cardSDK + + + +## State + +Early development state. + +There is just one method to run pdx with sim now. + + + +- - - + +This software is not sponsored or supported by Panic. diff --git a/support/sim-ctrl/src/lib.rs b/support/sim-ctrl/src/lib.rs new file mode 100644 index 00000000..cd056000 --- /dev/null +++ b/support/sim-ctrl/src/lib.rs @@ -0,0 +1,17 @@ +#![feature(error_generic_member_access)] +#![feature(exit_status_error)] + +#[macro_use] +#[cfg(feature = "tracing")] +extern crate tracing; + +#[macro_use] +#[cfg(not(feature = "tracing"))] +extern crate log; + +pub extern crate utils; + +pub use utils::toolchain::sdk::Sdk; + + +pub mod run; diff --git a/support/sim-ctrl/src/run.rs b/support/sim-ctrl/src/run.rs new file mode 100644 index 00000000..2fa9e73c --- /dev/null +++ b/support/sim-ctrl/src/run.rs @@ -0,0 +1,64 @@ +use std::path::Path; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use utils::toolchain::sdk::Sdk; + + +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub async fn run(pdx: &Path, sdk: Option<&Path>) -> Result<(), Error> { + #[allow(unused_mut)] + let mut cmd = command(&pdx, sdk.as_deref())?; + #[cfg(feature = "tokio")] + let mut cmd = tokio::process::Command::from(cmd); + + trace!("executing: {cmd:?}"); + + #[cfg(feature = "tokio")] + cmd.status().await?.exit_ok()?; + #[cfg(not(feature = "tokio"))] + cmd.status()?.exit_ok()?; + + Ok(()) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub fn command(pdx: &Path, sdk: Option<&Path>) -> Result { + let sdk = sdk.map_or_else(|| Sdk::try_new(), Sdk::try_new_exact)?; + + let (pwd, sim) = if cfg!(target_os = "macos") { + ("Playdate Simulator.app/Contents/MacOs", "./Playdate Simulator") + } else if cfg!(unix) { + (".", "./PlaydateSimulator") + } else if cfg!(windows) { + (".", "PlaydateSimulator.exe") + } else { + return Err(IoError::new(IoErrorKind::Unsupported, "Unsupported platform").into()); + }; + + let mut cmd = std::process::Command::new(sim); + cmd.current_dir(sdk.bin().join(pwd)); + cmd.arg(&pdx); + + Ok(cmd) +} + + +pub use error::*; +mod error { + #[derive(thiserror::Error, Debug)] + pub enum Error { + #[error(transparent)] + Io { + #[backtrace] + #[from] + source: std::io::Error, + }, + #[error(transparent)] + Exec { + #[backtrace] + #[from] + source: std::process::ExitStatusError, + }, + } +} diff --git a/support/tool/Cargo.toml b/support/tool/Cargo.toml index 4167ce07..0dafeb1b 100644 --- a/support/tool/Cargo.toml +++ b/support/tool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate-tool" -version = "0.1.4" +version = "0.2.0" readme = "README.md" description = "Tool for interaction with Playdate device and sim." keywords = ["playdate", "usb", "utility"] @@ -15,32 +15,52 @@ repository.workspace = true [[bin]] path = "src/main.rs" name = "pdtool" -required-features = ["cli"] [dependencies] -regex.workspace = true -log.workspace = true -env_logger = { workspace = true, optional = true } -thiserror = "1.0" +# RT, async: +tokio = { version = "1.36", features = ["full", "rt-multi-thread"] } +futures = { version = "0.3" } +futures-lite.workspace = true +# fmt: serde = { workspace = true, features = ["derive"] } serde_json.workspace = true -plist = "1.6" -rusb = { version = "0.9", optional = true } -usb-ids = { version = "1.2023.0", optional = true } +# CLI: +log.workspace = true +env_logger.workspace = true +thiserror.workspace = true +miette = { version = "7.2", features = ["fancy"] } -# used assert::env-resolver and sdk -build = { workspace = true, default-features = false } +# tracing: +tracing = { version = "0.1", optional = true } +tracing-subscriber = { version = "0.3", optional = true } +console-subscriber = { version = "0.2", features = [ + "env-filter", +], optional = true } [dependencies.clap] features = ["std", "env", "derive", "help", "usage", "color"] workspace = true -optional = true + +# PD: +[dependencies.device] +features = ["async", "tokio", "clap", "tokio-serial"] +workspace = true + +[dependencies.simulator] +features = ["tokio"] +workspace = true [features] -default = ["usb"] -cli = ["clap", "env_logger"] -usb = ["rusb", "usb-ids"] +tracing = [ + "dep:tracing", + "tracing-subscriber", + "device/tracing", + "simulator/tracing", +] + +# Useful with [tokio-console](https://tokio.rs/tokio/topics/tracing-next-steps) +tokio-tracing = ["tracing", "tokio/tracing", "console-subscriber", "tracing"] diff --git a/support/tool/README.md b/support/tool/README.md index 8ec6c9d7..710ed977 100644 --- a/support/tool/README.md +++ b/support/tool/README.md @@ -1,49 +1,79 @@ # Playdate Tool -CLI-tool and lib for interaction with Playdate device and sim. +Cross-platform CLI-tool for interaction with [Playdate][playdate-website] device and simulator. -### Status +Can do for you: +- operate with multiple devices simultaneously + - find connected devices, filter by mode, state, serial-number + - send commands + - read from devices + - mount as drive (mass storage usb) + - unmount + - install pdx (playdate package) + - run pdx (optionally with install pdx before run) +- operate with simulator + - run pdx + - read output from sim. -This is earlier version, that means "alpha" or "MVP". -API can be changed in future versions. -Global __refactoring is planned__ with main reason of properly work with usb on all platforms. - -Currently tested and works good on following platforms: -- Unix (x86-64 and aarch64) - - macos 👍 - - linux 👍 -- Windows (x86-64 and aarch64) - - ⚠️ known issues with hardware lookup, work in progress. +Tested on following platforms: +- MacOs +- Linux +- Windows ## Prerequisites To build playdate-tool you're need: 1. Rust __nightly__ toolchain -2. Probably `libusb` and `pkg-config` or `vcpkg`, follow [instructions for rusb crate][rusb]. - -To use playdate-tool you're need: -1. [Playdate SDK][sdk] - - Ensure that env var `PLAYDATE_SDK_PATH` points to the SDK root -2. This tool. +2. Linux only: + - `libudev`, follow [instructions for udev crate][udev-crate-deps]. +3. [Playdate SDK][sdk] with simulator + - Ensure that env var `PLAYDATE_SDK_PATH` points to the SDK root. _This is optional, but good move to help the tool to find SDK, and also useful if you have more then one version of SDK._ +[playdate-website]: https://play.date +[udev-crate-deps]: https://crates.io/crates/udev#Dependencies [sdk]: https://play.date/dev/#cardSDK -[doc-prerequisites]: https://sdk.play.date/Inside%20Playdate%20with%20C.html#_prerequisites -[rusb]: https://crates.io/crates/rusb - ## Installation ```bash -cargo install playdate-tool --features=cli +cargo install playdate-tool +``` + + +## Usage + +```bash +pdtool --help ``` +
Help output example + +```text +Usage: pdtool [OPTIONS] + +Commands: + list Print list of connected active Playdate devices + mount Mount a Playdate device if specified, otherwise mount all Playdates as possible + unmount Unmount a Playdate device if specified, otherwise unmount all mounted Playdates + install Install given package to device if specified, otherwise use all devices as possible + run Install and run given package on the specified device or simulator + read Connect to device and proxy output to stdout + send Send command to specified device + help Print this message or the help of the given subcommand(s) + +Options: + --format Standard output format [default: human] [possible values: human, json] + -h, --help Print help + -V, --version Print version +``` +
- - - diff --git a/support/tool/src/cli.rs b/support/tool/src/cli.rs new file mode 100644 index 00000000..a11437ec --- /dev/null +++ b/support/tool/src/cli.rs @@ -0,0 +1,230 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand, ValueEnum}; +use simulator::utils::consts::SDK_ENV_VAR; + +use crate::device::query::Query; + + +pub fn parse() -> Cfg { Cfg::parse() } + + +#[derive(Parser, Debug)] +#[command(author, version, about, name = "pdtool")] +pub struct Cfg { + #[command(subcommand)] + pub cmd: Command, + + /// Standard output format. + #[clap(long, global = true, default_value_t = Format::Human)] + pub format: Format, +} + + +#[derive(ValueEnum, Debug, Clone, Copy)] +pub enum Format { + Human, + Json, +} + +impl std::fmt::Display for Format { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Format::Human => "human".fmt(f), + Format::Json => "json".fmt(f), + } + } +} + + +#[derive(Subcommand, Debug)] +pub enum Command { + /// Print list of connected active Playdate devices. + List { + #[arg(default_value_t = DeviceKind::Any)] + kind: DeviceKind, + }, + + /// Mount a Playdate device if specified, otherwise mount all Playdates as possible. + Mount { + #[command(flatten)] + query: Query, + /// Wait for availability of mounted device's filesystem. + #[arg(long, default_value_t = false)] + wait: bool, + }, + + /// Unmount a Playdate device if specified, otherwise unmount all mounted Playdates. + Unmount { + /// Device spec + #[command(flatten)] + query: Query, + /// Wait for device to be connected after unmounted. + #[arg(long, default_value_t = false)] + wait: bool, + }, + + /// Install given package to device if specified, otherwise use all devices as possible. + /// + /// Workflow: switch to storage mode and mount if needed, write files, unmount if requested. + Install(#[command(flatten)] Install), + + /// Install and run given package on the specified device or simulator. + Run(#[command(flatten)] run::Run), + + /// Connect to device and proxy output to stdout. + Read(#[command(flatten)] Query), + + /// Send command to specified device. + // #[command(hide = true)] + Send(#[command(flatten)] Send), + + /// Debug functions, only for development purposes. + #[cfg(debug_assertions)] + Debug(#[command(flatten)] Dbg), +} + + +#[derive(Clone, Debug, clap::Parser)] +pub struct Dbg { + /// Command to send: + #[clap(subcommand)] + pub cmd: DbgCmd, + + /// Device selector. + #[command(flatten)] + pub query: Query, +} + +#[derive(Debug, Clone, clap::Subcommand)] +pub enum DbgCmd { + /// Inspect device(s) state. + Inspect, +} + + +#[derive(ValueEnum, Debug, Clone, Copy)] +pub enum DeviceKind { + Any, + Data, + Storage, +} + +impl ToString for DeviceKind { + fn to_string(&self) -> String { + match self { + DeviceKind::Any => "any", + DeviceKind::Data => "data", + DeviceKind::Storage => "storage", + }.to_owned() + } +} + + +#[derive(Clone, Debug, clap::Parser)] +#[command(author, version, about, long_about = None, name = "install")] +pub struct Install { + /// Path to the PDX package. + #[arg(value_name = "PACKAGE")] + pub pdx: PathBuf, + + /// Allow to overwrite existing files. + #[arg(long, default_value_t = false)] + pub force: bool, + + #[command(flatten)] + pub query: Query, +} + + +#[derive(Clone, Debug, clap::Parser)] +#[command(author, version, about, long_about = None, name = "send")] +pub struct Send { + /// Command to send: + #[clap(subcommand)] + pub command: device::device::command::Command, + + /// Device selector. + #[command(flatten)] + pub query: Query, + + /// Read output from device after sending command. + #[arg(long, default_value_t = false)] + pub read: bool, +} + + +pub use run::*; +mod run { + use std::borrow::Cow; + + use super::*; + + + #[derive(Clone, Debug, clap::Parser)] + #[command(author, version, about, long_about = None, name = "run")] + pub struct Run { + #[clap(subcommand)] + pub destination: Destination, + } + + + #[derive(Clone, Debug, clap::Subcommand)] + pub enum Destination { + /// Install to the device. + /// + /// Attention: parameter is a local path to pdx-package. + /// But in case of '--no-install' given path will be interpreted as on-device relative to it's root path, + /// e.g. "/Games/my-game.pdx". + #[clap(visible_alias("dev"))] + Device(Dev), + + /// Run with simulator. + /// Playdate required to be installed. + #[clap(visible_alias("sim"))] + Simulator(Sim), + } + + impl std::fmt::Display for Destination { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name: Cow = match self { + Destination::Device(Dev { install: Install { query, .. }, + .. }) => format!("device:{query}").into(), + Destination::Simulator(_) => "simulator".into(), + }; + name.fmt(f) + } + } + + + #[derive(Clone, Debug, clap::Parser)] + /// Simulator destination + pub struct Sim { + /// Path to the PDX package. + #[arg(value_name = "PACKAGE")] + pub pdx: PathBuf, + + /// Path to Playdate SDK + #[arg(long, env = SDK_ENV_VAR, value_name = "DIRECTORY", value_hint = clap::ValueHint::DirPath)] + pub sdk: Option, + } + + + #[derive(Clone, Debug, clap::Parser)] + /// Hardware destination + pub struct Dev { + #[command(flatten)] + pub install: super::Install, + + /// Do not install pdx to the device. + /// If set, path will be interpreted as on-device path of already installed package, + /// relative to the root of device's fs partition. + #[arg(long, name = "no-install", default_value_t = false)] + pub no_install: bool, + + /// Do not wait & read the device's output after execution. + /// Exits immediately after send 'run' command. + #[arg(long, name = "no-read", default_value_t = false)] + pub no_read: bool, + } +} diff --git a/support/tool/src/cli/commands/install.rs b/support/tool/src/cli/commands/install.rs deleted file mode 100644 index f596f3e0..00000000 --- a/support/tool/src/cli/commands/install.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::path::PathBuf; -use std::time::Duration; -use std::process::Command; - -use ::build::toolchain::sdk::Sdk; -use ::build::compile::PDX_PKG_EXT; - -use crate::Error; -use crate::OnDevicePath; -use crate::wait_for; -use crate::wait_for_mut; -use super::mount::Mount; -use crate::mount::MountHandle; -use crate::cli::find_one_device; - - -/// Installs a given pdx-package to the device. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "cli", derive(clap::Parser))] -#[cfg_attr(feature = "cli", command(author, version, about, long_about = None, name = "install"))] -pub struct Install { - /// Path to the PDX package. - #[cfg_attr(feature = "cli", arg(value_name = "PACKAGE"))] - pub pdx: PathBuf, - - #[cfg_attr(feature = "cli", command(flatten))] - pub mount: Mount, -} - - -pub fn install(cfg: &Install) -> Result { - if let Some(sdk) = Sdk::try_new().map_err(|err| error!("{err}")).ok() { - install_with(&sdk, cfg) - } else { - install_without_sdk(cfg) - } -} - -// -// -// TODO: before try mount check the device already mounted. -// -// - -pub fn install_without_sdk(_cfg: &Install) -> Result { - // - todo!() -} - - -#[cfg(unix)] -pub fn install_with(sdk: &Sdk, cfg: &Install) -> Result { - if !cfg.pdx.try_exists()? { - return Err(Error::Error(format!("Input file not exists: {}", cfg.pdx.display()))); - } - if !cfg.pdx.is_dir() || - cfg.pdx - .extension() - .map(|s| s.to_string_lossy().to_lowercase()) - .as_deref() != - Some(PDX_PKG_EXT) - { - return Err(Error::Error(format!( - "Sims to input directory is not PDX package: {}", - cfg.pdx.display() - ))); - } - - let mut device = find_one_device(cfg.mount.device.clone())?; - - if device.mounted() { - device.refresh()?; - } - - let mut mount_handle = if device.mounted() { - debug!("{device} is already mounted"); - if let Some(vol) = device.volume.as_deref() { - let mut handle = MountHandle::new(vol.to_owned()); - handle.unmount_on_drop(false); - handle - } else { - unreachable!() - } - } else { - crate::mount::mount_with(&sdk, &cfg.mount.mount, &device)? - }; - - // mount the device: - // let mount_handle = crate::mount::mount_with(&sdk, &cfg.mount.mount, &device)?; - let mount = mount_handle.path(); - - // wait mounting: - let duration = Duration::from_millis(100); - let max = Duration::from_secs(6); - // let is_mounted = || mount.try_exists().ok().unwrap_or_default(); - let mut is_mounted = { - let device = &mut device; - move || mount.try_exists().ok().unwrap_or_default() || (device.refresh().is_ok() && device.mounted()) - }; - // first try: - let need_try = wait_for_mut(&mut is_mounted, duration, max).or_else(|err| { - error!("{err}"); - warn!("If your OS does not automatically mount your Playdate, please do so now."); - // wait_for(&mut is_mounted, duration, Duration::from_secs(60 * 2)) - Ok::<_, Error>(()) - }) - .is_err(); - // last try: - if need_try { - wait_for_mut(&mut is_mounted, duration, Duration::from_secs(60 * 2))?; - } - - // update mount-point if needed: - if let Some(vol) = device.volume.as_deref() { - if vol != mount_handle.path() { - debug!("Mount point changed to {}", vol.display()); - mount_handle.set_mount_point(vol.to_owned()); - } - } - - - info!("Device {device} mounted successfully"); - let _ = std::fs::read_dir(mount_handle.path()).map(|entries| entries.count()) - .ok(); - - - let games = mount_handle.path().join("Games"); - // This prevents issues that occur when the PLAYDATE volume is mounted - // but not all of the inner folders are available yet. - let is_fs_ok = || games.try_exists().ok().unwrap_or_default(); - let max = Duration::from_secs(10); - debug!("Waiting fs availability for {device}..."); - wait_for(is_fs_ok, duration, max).map_err(|err| { - error!("Device {device} directory '{}' not found, {err}", games.display()); - err - })?; - - - // copying the game: - info!("Copying PDX to {device}, do not eject."); - let pdx_filename = cfg.pdx.file_name().expect("filename"); - let target = games.join(&pdx_filename); - debug!("Copying PDX to '{}'", target.display()); - std::fs::copy(&cfg.pdx, &target).map_err(Error::from) - .or_else(|_| { - // -f for force - #[cfg(unix)] - Command::new("cp").arg("-r") - .arg(&cfg.pdx) - .arg(&games) - .status()? - .exit_ok()?; - Ok::<_, Error>(0) - })?; - info!("Copied: {}", pdx_filename.to_string_lossy()); - - Ok(OnDevicePath { path: PathBuf::from(games.file_name().unwrap()).join(&pdx_filename), - device }) -} - - -#[cfg(windows)] -pub fn install_with(sdk: &Sdk, cfg: &Install) -> Result { - if !cfg.pdx.try_exists()? { - return Err(Error::Error(format!("Input file not exists: {}", cfg.pdx.display()))); - } - if cfg.pdx - .extension() - .map(|s| s.to_string_lossy().to_lowercase()) - .as_deref() != - Some(PDX_PKG_EXT) - { - return Err(Error::Error(format!("Sims to input file is not PDX: {}", cfg.pdx.display()))); - } - - Command::new(sdk.pdutil()).arg("install") - .arg(&cfg.pdx) - .status()? - .exit_ok()?; - - Ok(OnDevicePath { path: PathBuf::from("Games").join(&cfg.pdx.file_name().expect("filename")), - device: todo!() }) -} diff --git a/support/tool/src/cli/commands/mod.rs b/support/tool/src/cli/commands/mod.rs deleted file mode 100644 index fe0f8329..00000000 --- a/support/tool/src/cli/commands/mod.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::path::Path; -use std::path::PathBuf; -use std::str::FromStr; -use crate::Error; -use crate::model::Device; -use crate::model::Mode; -use crate::model::SerialNumber; -use crate::search::fs::is_tty_fd; -use crate::search::list_connected_devices; - -pub mod mount; -pub mod install; -pub mod run; -pub mod read; - - -#[derive(Clone, Debug)] -#[cfg_attr(feature = "cli", derive(clap::Parser))] -#[cfg_attr(feature = "cli", command(author, version, about, long_about = None, name = "device"))] -pub struct DeviceQuery { - /// Serial number of usb device or absolute path to file-descriptor - /// - /// Value format example: 'PDUN-XNNNNNNN' or '/dev/cu.usbmodemPDUN_XNNNNNNN' - #[cfg_attr(feature = "cli", arg(env = crate::DEVICE_SERIAL_ENV, name = "device"))] - pub value: Option, -} - -impl Default for DeviceQuery { - fn default() -> Self { - Self { value: std::env::var(crate::DEVICE_SERIAL_ENV).map(|s| DeviceQueryValue::from_str(&s).ok()) - .ok() - .flatten() } - } -} - - -#[derive(Clone, Debug)] -pub enum DeviceQueryValue { - Serial(SerialNumber), - Path(PathBuf), -} - -type ParseError = ::Err; -impl FromStr for DeviceQueryValue { - type Err = crate::Error; - - fn from_str(name: &str) -> Result { - let name = name.trim(); - if name.is_empty() { - return Err(ParseError::from(name).into()); - } - - let serial = SerialNumber::try_from(name)?; - let path = Path::new(name); - let is_direct = path.is_absolute() && is_tty_fd(path)?; - - if is_direct { - Ok(DeviceQueryValue::Path(path.to_owned())) - } else { - Ok(DeviceQueryValue::Serial(serial)) - } - } -} - -impl DeviceQueryValue { - pub fn to_printable_string(&self) -> String { - match self { - Self::Serial(sn) => sn.to_string(), - Self::Path(p) => p.display().to_string(), - } - } -} - - -/// Automatically find ony one device. Error if found many. -pub fn find_one_device(query: DeviceQuery) -> Result { - let devices = list_connected_devices()?; - let mut device = if let Some(query) = query.value { - use DeviceQueryValue as Query; - - let device = match query { - Query::Serial(serial) => devices.into_iter().find(|device| device.serial == serial), - Query::Path(path) => { - if !is_tty_fd(&path)? { - return Err(Error::Error(format!("{} is not a cu/tty", path.display())).into()); - } - - devices.into_iter() - .map(|mut device| { - if device.mode == Mode::Data && device.tty.is_none() { - device.refresh_tty().ok(); - } - device - }) - .find(|device| { - if matches!(device.tty.as_ref(), Some(ref p) if p.as_path() == path.as_path()) { - true - } else { - false - } - - // TODO: create new device with path - }) - .or_else(|| { - let maybe = SerialNumber::try_from(path.as_path()).ok() - .unwrap_or_else(SerialNumber::unknown); - Some(Device { serial: maybe, - mode: Mode::Data, - tty: Some(path.to_path_buf()), - volume: None }) - }) - }, - }; - device.ok_or(Error::device_not_found())? - } else { - if devices.is_empty() { - return Err(Error::device_not_found().into()); - } else if devices.len() > 1 { - return Err(Error::multiple_devices(devices).into()); - } - devices.into_iter().next().ok_or(Error::device_not_found())? - }; - - if device.tty.is_none() { - device.refresh_tty().ok(); - } - - Ok(device) -} diff --git a/support/tool/src/cli/commands/mount.rs b/support/tool/src/cli/commands/mount.rs deleted file mode 100644 index 373ed236..00000000 --- a/support/tool/src/cli/commands/mount.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::ffi::OsString; -use std::path::PathBuf; - -use ::build::toolchain::sdk::Sdk; - -use super::DeviceQuery; -use crate::Error; -use crate::cli::find_one_device; -use crate::model::Device; -use crate::mount::mount_with; -use crate::mount::mount_without_sdk; - - -/// Mount the device. -#[derive(Clone, Debug, clap::Parser)] -#[command(author, version, about, long_about = None, name = "install")] -pub struct Mount { - #[cfg_attr(feature = "cli", command(flatten))] - pub device: DeviceQuery, - - /// Expected mount point for serial device. - #[cfg_attr(feature = "cli", arg(long, env = crate::DEVICE_MOUNT_POINT_ENV))] - #[cfg_attr(feature = "cli", arg(default_value = crate::DEVICE_MOUNT_POINT_DEF))] - pub mount: PathBuf, -} - -impl Default for Mount { - fn default() -> Self { - Self { device: Default::default(), - mount: - std::env::var_os(crate::DEVICE_MOUNT_POINT_ENV).unwrap_or(OsString::from(crate::DEVICE_MOUNT_POINT_DEF)) - .into() } - } -} - - -pub fn mount(cfg: &Mount) -> Result { - let device = find_one_device(cfg.device.clone())?; - trace!("mounting device: {device:?}"); - let handle = mount_device(&cfg, &device)?; - - info!( - "Successfully mounted, expected mount-point: {}", - handle.path().display() - ); - Ok(handle) -} - - -fn mount_device(cfg: &Mount, device: &Device) -> Result { - let mount_handle = if let Some(sdk) = Sdk::try_new().map_err(|err| error!("{err}")).ok() { - mount_with(&sdk, &cfg.mount, device) - } else { - mount_without_sdk(&cfg.mount, device) - }?; - Ok(mount_handle) -} diff --git a/support/tool/src/cli/commands/read.rs b/support/tool/src/cli/commands/read.rs deleted file mode 100644 index f4f4045f..00000000 --- a/support/tool/src/cli/commands/read.rs +++ /dev/null @@ -1,15 +0,0 @@ -use super::DeviceQuery; - - -/// Read the device or simulator output. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "cli", derive(clap::Parser))] -#[cfg_attr(feature = "cli", command(author, version, about, long_about = None, name = "read"))] -pub struct Read { - #[cfg_attr(feature = "cli", command(flatten))] - pub device: DeviceQuery, - - #[arg(long)] - /// Set 'echo' mode for external shell. - pub echo: Option, -} diff --git a/support/tool/src/cli/commands/run.rs b/support/tool/src/cli/commands/run.rs deleted file mode 100644 index 3c358835..00000000 --- a/support/tool/src/cli/commands/run.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::borrow::Cow; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; -use std::process::Stdio; -use std::time::Duration; - -use ::build::toolchain::sdk::Sdk; - -use crate::Error; -use crate::OnDevicePath; -use crate::wait_for_mut; -use super::find_one_device; -use super::install::Install; - - -/// Run a given pdx-package to the device or simulator. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "cli", derive(clap::Parser))] -#[cfg_attr(feature = "cli", command(author, version, about, long_about = None, name = "run"))] -pub struct Run { - #[cfg_attr(feature = "cli", clap(subcommand))] - pub destination: Destination, -} - - -#[derive(Clone, Debug)] -#[cfg_attr(feature = "cli", derive(clap::Subcommand))] -pub enum Destination { - /// Install to the device. - /// - /// Attention: parameter is a local path to pdx-package. - /// But in case of '--no-install' given path will interpret as on-device relative to it's root path, - /// e.g. "Games/my-game.pdx". - #[cfg_attr(feature = "cli", clap(alias("dev")))] - Device(DeviceDestination), - - /// Run with simulator. - #[cfg_attr(feature = "cli", clap(alias("sim")))] - Simulator(SimDestination), -} - -impl std::fmt::Display for Destination { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let name = match self { - Destination::Device(_) => "device", - Destination::Simulator(_) => "simulator", - }; - write!(f, "{name}") - } -} - -#[derive(Clone, Debug)] -#[cfg_attr(feature = "cli", derive(clap::Parser))] -pub struct DeviceDestination { - #[cfg_attr(feature = "cli", command(flatten))] - pub install: Install, - - /// Do not install pdx to the device. - /// If so, path will be interpreted as on-device path of already installed package, - /// relative to the root of device's data partition. - /// Add '--help' for more info. - #[cfg_attr(feature = "cli", arg(long, name = "no-install", default_value_t = false))] - pub no_install: bool, - - /// Do not wait connect & listen to the device after execution. - /// Exits immediately after send 'run' command. - #[cfg_attr(feature = "cli", arg(long, name = "no-wait", default_value_t = false))] - pub no_wait: bool, -} - - -#[derive(Clone, Debug)] -#[cfg_attr(feature = "cli", derive(clap::Parser))] -pub struct SimDestination { - /// Path to the PDX package. - #[cfg_attr(feature = "cli", arg(value_name = "PACKAGE"))] - pub pdx: PathBuf, -} - - -pub fn run(cfg: Run) -> Result<(), Error> { - match cfg.destination { - Destination::Device(cfg) => { - run_on_device(cfg)?; - }, - Destination::Simulator(cfg) => { - run_on_sim(cfg)?; - }, - } - Ok(()) -} - - -pub fn run_on_sim(cfg: SimDestination) -> Result<(), Error> { - let sdk = Sdk::try_new()?; - - let (pwd, sim) = if cfg!(target_os = "macos") { - ("Playdate Simulator.app/Contents/MacOs", "./Playdate Simulator") - } else if cfg!(unix) { - (".", "./PlaydateSimulator") - } else if cfg!(windows) { - (".", "PlaydateSimulator.exe") - } else { - return Err(Error::Err("Unsupported platform")); - }; - - let mut cmd = Command::new(sim); - cmd.current_dir(sdk.bin().join(pwd)); - cmd.arg(&cfg.pdx); - - debug!("Run: {cmd:?}"); - cmd.status()?.exit_ok()?; - Ok(()) -} - - -pub fn run_on_device(cfg: DeviceDestination) -> Result { - let path = if let Some(sdk) = Sdk::try_new().map_err(|err| error!("{err}")).ok() { - run_on_device_with(&sdk, &cfg) - } else { - run_on_device_without_sdk(&cfg) - }?; - - #[cfg(any(not(windows), feature = "usb"))] - if !cfg.no_wait { - path.device.read_to_stdout(Some(false))?; - } - - debug!("Run on {}: finished", &path.device); - Ok(path) -} - - -fn find_ondevice_path(cfg: &DeviceDestination) -> Result { - let device = find_one_device(cfg.install.mount.device.clone())?; - let path = if cfg.install.pdx.try_exists().ok().unwrap_or_default() { - warn!("Sims to given path to pdx-package is a local path, but on-device path expected."); - - // try convert to on-device path if possible: - let input_path = &cfg.install.pdx; - if input_path.components().count() > 2 && !input_path.starts_with("Games/") { - let new = Path::new("Games").join(input_path.file_name().expect("filename")); - debug!("Adapted path: {} <- {}", new.display(), input_path.display()); - Cow::from(new) - } else { - cfg.install.pdx.as_path().into() - } - } else { - cfg.install.pdx.as_path().into() - }.into(); - Ok(OnDevicePath { path, device }) -} - - -pub fn run_on_device_without_sdk(cfg: &DeviceDestination) -> Result { - let path = if cfg.no_install { - find_ondevice_path(&cfg)? - } else { - let mut installed = super::install::install_without_sdk(&cfg.install)?; - - // wait for unmount and connected again - let err_msg = format!("Unable to connect, device {} not found.", &installed.device); - - let mut is_connected = { - let installed = &mut installed; - move || { - installed.device.refresh().ok(); - installed.device.is_cu_ok() - } - }; - - - let duration = Duration::from_millis(50); - let max = Duration::from_secs(10); - // first try: - let need_try = wait_for_mut(&mut is_connected, duration, max).map_err(|err| { - debug!("{err:?}"); - warn!("{err_msg}"); - }) - .is_err(); - // last try: - if need_try { - wait_for_mut(&mut is_connected, duration, max).map_err(|err| { - debug!("{err:?}"); - error!("{err_msg}"); - err - })?; - } - - debug!("{:?} was found", &installed.device); - installed - }; - - // run: - path.device - .write(format!("run {}\n", path.path.display())) - .map_err(|err| { - error!("{err}"); - Error::Error(format!("Unable to send command to {}.", &path.device)) - })?; - - Ok(path) -} - - -pub fn run_on_device_with(sdk: &Sdk, cfg: &DeviceDestination) -> Result { - let path = if cfg.no_install { - find_ondevice_path(&cfg)? - } else { - let mut installed = super::install::install_with(&sdk, &cfg.install)?; - - // wait for unmount and connected again - let err_msg = format!("Unable to connect, device {} not found.", &installed.device); - - let mut is_connected = { - let installed = &mut installed; - move || { - installed.device.refresh().ok(); - installed.device.is_cu_ok() - } - }; - - - let duration = Duration::from_millis(50); - let max = Duration::from_secs(10); - // first try: - let need_try = wait_for_mut(&mut is_connected, duration, max).map_err(|err| { - debug!("{err:?}"); - warn!("{err_msg}"); - }) - .is_err(); - // last try: - if need_try { - wait_for_mut(&mut is_connected, duration, max).map_err(|err| { - debug!("{err:?}"); - error!("{err_msg}"); - err - })?; - } - - debug!("{:?} was found", &installed.device); - installed - }; - - // run: - - if cfg!(feature = "usb") { - path.device - .write(format!("run {}\n", path.path.display())) - .map_err(|err| { - error!("{err}"); - Error::Error(format!("Unable to send command to {}.", &path.device)) - })?; - debug!("run cmd sent, waiting for boot finished"); - // this needed for case when we can catch a device immediately after reboot and OS did not loaded yet, - // but we're writing `run` to buffer, so OS will read it after successful load and execute the command. - // But if we write another command before OS read previous, it will overwritten and not read by OS. - // And I don't know how to determine state of OS. (Perhaps just ask `ping`?) - // So we must to wait. - // TODO: implement write `ping` and read answer - std::thread::sleep(Duration::from_secs(2)); - debug!("continuing"); - } else { - let mut cmd = Command::new(sdk.pdutil()); - let tty = path.device - .tty - .as_deref() - .ok_or(Error::unable_to_find_tty(&path.device))?; - cmd.arg(&tty).arg("run").arg(&path.path); - cmd.stdout(Stdio::inherit()); - cmd.stderr(Stdio::inherit()); - debug!("Run: {cmd:?}"); - cmd.status()?.exit_ok()?; - } - - Ok(path) -} diff --git a/support/tool/src/cli/mod.rs b/support/tool/src/cli/mod.rs deleted file mode 100644 index e1f944c3..00000000 --- a/support/tool/src/cli/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -use clap::{Parser, Subcommand}; - -mod commands; -pub use commands::*; - - -pub fn parse() -> Cfg { Cfg::parse() } - - -#[derive(Parser, Debug)] -#[command(author, version, about, name = "pdtool")] -pub struct Cfg { - // #[command(flatten)] - // device: DeviceQuery, - #[command(subcommand)] - pub command: Command, -} - - -#[derive(Subcommand, Debug)] -pub enum Command { - /// Print list of connected active Playdate devices. - List, - - Mount(mount::Mount), - - Install(install::Install), - - Run(run::Run), - - /// Connect to device and proxy output to stdout. - Read(read::Read), -} diff --git a/support/tool/src/io/mod.rs b/support/tool/src/io/mod.rs deleted file mode 100644 index 303b0c18..00000000 --- a/support/tool/src/io/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::fs::File; -use std::io::prelude::*; -use std::io::BufReader; -use std::path::Path; -use crate::Error; -use crate::model::Device; - - -impl Device { - pub fn write>(&self, command: S) -> Result<(), Error> { - debug!("send command: '{}'", command.as_ref()); - let first_try = { - debug!(" first try..."); - #[cfg(feature = "usb")] - let res = crate::usb::write(self, command.as_ref()).map_err(Error::from); - #[cfg(not(feature = "usb"))] - let res: Result<(), Error> = Err(Error::Err("`usb` feature disabled")); - res - }; - - match first_try { - Ok(_) => Ok(()), - Err(err) => { - warn!("{err}, trying fallback"); - // we have fallback solution - #[cfg(unix)] - let res = unix::tty_write( - &self.tty.as_deref().ok_or(Error::unable_to_find_tty(self))?, - command, - ); - // or not - #[cfg(not(unix))] - let res = Err(err); - res - }, - } - } - - pub fn read_to_stdout(&self, echo: Option) -> Result<(), Error> { - let first_try = { - #[cfg(feature = "usb")] - let res = crate::usb::read_output(self, echo).map_err(Error::from); - #[cfg(not(feature = "usb"))] - let res: Result<(), Error> = Err(Error::Err("`usb` feature disabled")); - res - }; - - match first_try { - Ok(_) => Ok(()), - Err(err) => { - warn!("{err}, trying fallback"); - // we have fallback solution - #[cfg(unix)] - let res = unix::tty_to_stdout_by_device(self, echo); - // or not - #[cfg(not(unix))] - let res = Err(err); - res - }, - } - } -} - - -#[cfg(unix)] -mod unix { - use std::process::Command; - - use super::*; - - - pub(super) fn tty_to_stdout_by_device(device: &Device, echo: Option) -> Result<(), Error> { - let tty = device.tty.as_ref().ok_or(Error::unable_to_find_tty(device))?; - tty_to_stdout(tty, echo) - } - - - pub(super) fn tty_to_stdout(tty: &Path, echo: Option) -> Result<(), Error> { - debug!("Redirecting {} output to this output", tty.display()); - - if let Some(echo) = echo { - let v = if echo { "on" } else { "off" }; - tty_write(tty, format!("echo {v}")).ok(); - } - - // Cat can everything! - Command::new("cat").arg(&tty) - .stdout(std::process::Stdio::inherit()) - .status()? - .exit_ok()?; - - // Sometimes cat see something like EOF, that impossible, so fallback: - let mut reader = BufReader::new(File::open(&tty)?); - println!(""); - loop { - if reader.has_data_left()? { - let mut buf = String::new(); - reader.read_to_string(&mut buf)?; - print!("{buf}"); - } - } - } - - - pub(super) fn tty_write>(tty: &Path, command: S) -> Result<(), Error> { - let command = command.as_ref(); - trace!("Write to {}: '{command}'", tty.display()); - - - let mut file = File::options().read(false) - .write(true) - .create(false) - .create_new(false) - .open(&tty)?; - file.write_all(command.trim().as_bytes())?; - file.write("\n".as_bytes())?; - file.flush()?; - - trace!("Write to {}: complete.", tty.display()); - Ok(()) - } -} diff --git a/support/tool/src/lib.rs b/support/tool/src/lib.rs deleted file mode 100644 index 0471ed15..00000000 --- a/support/tool/src/lib.rs +++ /dev/null @@ -1,123 +0,0 @@ -#![feature(exit_status_error)] -#![feature(buf_read_has_data_left)] -#![feature(extract_if)] - -#[macro_use] -extern crate log; - -use std::path::PathBuf; -use std::time::Duration; - -pub mod model; -pub mod search; -pub mod mount; -pub mod usb; - -use model::Device; - -#[cfg(feature = "cli")] -pub mod cli; -pub mod io; - -pub use mount::DEVICE_MOUNT_POINT_DEF; -pub const DEVICE_SERIAL_ENV: &str = "PLAYDATE_SERIAL_DEVICE"; -pub const DEVICE_MOUNT_POINT_ENV: &str = "PLAYDATE_MOUNT_POINT"; - - -pub struct OnDevicePath { - pub device: Device, - - /// Path relative to the device mount point. - /// __Not absolute path.__ - pub path: PathBuf, -} - - -pub(crate) fn wait_for bool>(f: F, delay: Duration, max: Duration) -> Result<(), Error> { - wait_for_mut(f, delay, max) -} - -pub(crate) fn wait_for_mut bool>(mut f: F, delay: Duration, max: Duration) -> Result<(), Error> { - let start = std::time::Instant::now(); - let mut res = false; - while !res && start.elapsed() < max { - res = f(); - if !res { - std::thread::sleep(delay); - } - } - if !res { Err(Error::timeout()) } else { Ok(()) } -} - - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("{0}")] - Io(#[from] std::io::Error), - - #[error("{0}")] - ExitStatus(#[from] std::process::ExitStatusError), - - #[error("{0}")] - Utf8(#[from] std::str::Utf8Error), - - #[error("{0}")] - #[cfg(feature = "usb")] - Usb(#[from] rusb::Error), - - #[error("{0}")] - Format(#[from] model::DeviceSerialFormatError), - - #[error("{0}")] - Json(#[from] serde_json::Error), - - #[error("{0}")] - Plist(#[from] plist::Error), - - #[error("Env var '{1}': {0}.")] - Env(std::env::VarError, &'static str), - - #[error("{0}")] - Error(String), - - #[error("{0}")] - Err(&'static str), -} - -impl From for Error { - fn from(value: String) -> Self { Self::Error(value) } -} - -impl From<&'static str> for Error { - fn from(value: &'static str) -> Self { Self::Err(value) } -} - -impl Error { - pub const fn device_not_found() -> Self { Self::Err("Playdate device not found") } - pub const fn device_already_mounted() -> Self { Self::Err("Playdate device already mounted") } - fn unable_to_find_tty(device: D) -> Self { - Self::Error(format!("Unable to find tty for '{device}'")) - } - fn named_device_not_found(name: String) -> Self { Self::Error(format!("Playdate device '{name}' not found")) } - fn env(name: &'static str, err: std::env::VarError) -> Self { Self::Env(err, name) } - - fn invalid_device_name() -> Self { - Self::Err("Invalid device name, should be in format PDUN_XNNNNNN or path to file-descriptor e.g.: /dev/cu.usbmodemPDUN_XNNNNNN.") - } - - pub fn multiple_devices>(devices: T) -> Self { - let s = format!( - "Many devices are found: [{}].\n Please choose one and pass as argument.", - devices.as_ref() - .into_iter() - .map(|d| format!("{d}")) - .collect::>() - .join(", ") - ); - - - Self::Error(format!("Playdate device '{s}' not found")) - } - - fn timeout() -> Self { Self::Err("Max timeout reached") } -} diff --git a/support/tool/src/main.rs b/support/tool/src/main.rs index 46138808..e82956d6 100644 --- a/support/tool/src/main.rs +++ b/support/tool/src/main.rs @@ -1,52 +1,408 @@ +#![feature(exitcode_exit_method)] #![feature(exit_status_error)] -#![cfg(feature = "cli")] +#[cfg(feature = "tracing")] +#[macro_use] +extern crate tracing; +#[cfg(not(feature = "tracing"))] #[macro_use] extern crate log; -extern crate playdate_tool as tool; -use tool::{Error, cli, search}; -use tool::cli::{install, mount, run}; -use tool::cli::find_one_device; +extern crate device as pddev; + +use std::path::PathBuf; +use std::process::Termination; + +use futures::{FutureExt, StreamExt, TryFutureExt}; +use pddev::device::query::Value; +use pddev::device::serial::SerialNumber; +use pddev::error::Error; +use pddev::device::query::Query; +use pddev::*; + + +use miette::IntoDiagnostic; +use report::AsReport; -fn main() -> Result<(), Error> { +mod cli; +mod report; + + +#[cfg(all(feature = "tracing", not(feature = "console-subscriber")))] +fn enable_tracing() { + use tracing::Level; + use tracing_subscriber::fmt::Subscriber; + + let subscriber = Subscriber::builder().compact() + .with_file(true) + .with_target(false) + .with_line_number(true) + .without_time() + .with_level(true) + .with_thread_ids(false) + .with_thread_names(true) + .with_max_level(Level::TRACE) + .finish(); + tracing::subscriber::set_global_default(subscriber).unwrap(); +} + +#[cfg(all(feature = "tracing", feature = "console-subscriber"))] +fn enable_tracing() { + use tracing::Level; + use console_subscriber::ConsoleLayer; + use tracing_subscriber::prelude::*; + + let console_layer = ConsoleLayer::builder().with_default_env().spawn(); + let fmt = tracing_subscriber::fmt::layer().with_file(true) + .with_target(false) + .with_line_number(true) + .without_time() + .with_level(true) + .with_thread_ids(true) + .with_thread_names(true) + .with_filter(tracing::level_filters::LevelFilter::from(Level::TRACE)); + + + tracing_subscriber::registry().with(console_layer) + .with(fmt) + .init(); +} + + +#[tokio::main] +async fn main() -> miette::Result<()> { + #[cfg(feature = "tracing")] + enable_tracing(); + #[cfg(not(feature = "tracing"))] + { + #[cfg(debug_assertions)] + std::env::set_var("RUST_LOG", "trace"); + env_logger::Builder::from_env(env_logger::Env::default()).format_indent(Some(3)) + .format_module_path(false) + .format_target(true) + .format_timestamp(None) + .init(); + } + let cfg = cli::parse(); - env_logger::init(); - trace!("input: {cfg:#?}"); - start(cfg).map_err(|err| { - error!("{err}"); - err - }) + + debug!("cmd: {:?}", cfg.cmd); + + match cfg.cmd { + cli::Command::List { kind } => list(cfg.format, kind).await, + cli::Command::Read(query) => read(query).await, + cli::Command::Mount { query, wait } => mount(query, wait, cfg.format).await, + cli::Command::Unmount { query, wait } => unmount(query, wait, cfg.format).await, + cli::Command::Install(cli::Install { pdx, query, force }) => install(query, pdx, force, cfg.format).await, + cli::Command::Run(cli::Run { destination: + cli::Destination::Device(cli::Dev { install: + cli::Install { pdx, + query, + force, }, + no_install, + no_read, }), }) => { + run_dev(query, pdx, no_install, no_read, force, cfg.format).await + }, + cli::Command::Run(cli::Run { destination: cli::Destination::Simulator(cli::Sim { pdx, sdk }), }) => { + simulator::run::run(&pdx, sdk.as_deref()).await + .inspect(|_| trace!("sim execution is done")) + .report() + .exit_process(); + }, + cli::Command::Send(cli::Send { command, query, read }) => send(query, command, read, cfg.format).await, + + cli::Command::Debug(cli::Dbg { cmd, query }) => debug::debug(cmd, query).await, + }.into_diagnostic() +} + + +mod debug { + use super::*; + + + pub async fn debug(cmd: cli::DbgCmd, _query: Query) -> Result<(), Error> { + use cli::DbgCmd as Cmd; + match cmd { + Cmd::Inspect => inspect().await?, + } + Ok(()) + } + + pub async fn inspect() -> Result<(), Error> { + use usb::discover::devices; + use mount::volume::volumes_for_map; + volumes_for_map(devices()?).await? + .into_iter() + .map(|(dev, vol)| (dev, vol.map(|v| v.path().to_path_buf()))) + .for_each(|(dev, path)| { + println!("dev: {dev:#?}"); + println!("vol: {path:?}"); + }); + + Ok(()) + } +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +async fn run_dev(query: Query, + pdx: PathBuf, + no_install: bool, + no_read: bool, + force: bool, + format: cli::Format) + -> Result<(), error::Error> { + let devs = run::run(query, pdx, no_install, no_read, force).await? + .into_iter() + .enumerate(); + if matches!(format, cli::Format::Json) { + print!("["); + } + + for (i, dev) in devs { + let repr = dev.as_report_short(); + match format { + cli::Format::Human => println!("{}", repr.to_printable_line()), + cli::Format::Json => { + if i > 0 { + println!(","); + } + serde_json::to_string(&repr).map(|s| println!("{s},")) + .map_err(|err| error!("{err}")) + .ok(); + }, + } + } + + if matches!(format, cli::Format::Json) { + println!("]"); + } + + Ok(()) } -fn start(cfg: cli::Cfg) -> Result<(), Error> { - match cfg.command { - cli::Command::List => { - search::list_connected_devices()?.into_iter() - .for_each(|device| println!("{device:?}")); + +/// `mount_and_install` with report. +#[cfg_attr(feature = "tracing", tracing::instrument())] +async fn install(query: Query, path: PathBuf, force: bool, format: cli::Format) -> Result<(), error::Error> { + if matches!(format, cli::Format::Json) { + print!("["); + } + install::mount_and_install(query, &path, force).await? + .filter_map(|res| { + async { res.map_err(|err| error!("{err}")).ok() } + }) + .enumerate() + .for_each_concurrent(4, |(i, installed)| { + let (drive, installed) = installed.into_parts(); + trace!("installed: {installed}"); + async move { + let repr = drive.as_report_short(); + match format { + cli::Format::Human => { + println!("{}", repr.to_printable_line()) + }, + cli::Format::Json => { + if i > 0 { + println!(","); + } + serde_json::to_string(&repr).map(|s| println!("{s},")) + .map_err(|err| error!("{err}")) + .ok(); + }, + } + } + }) + .await; + if matches!(format, cli::Format::Json) { + println!("]"); + } + Ok(()) +} + + +/// [[mount::mount_and]] with report. +#[cfg_attr(feature = "tracing", tracing::instrument())] +async fn mount(query: Query, wait: bool, format: cli::Format) -> Result<(), error::Error> { + if matches!(format, cli::Format::Json) { + print!("["); + } + mount::mount_and(query, wait).await? + .enumerate() + .for_each_concurrent(4, |(i, res)| { + async move { + match res { + Ok(drive) => { + let repr = drive.as_report_short(); + match format { + cli::Format::Human => println!("{}", repr.to_printable_line()), + cli::Format::Json => { + if i > 0 { + println!(","); + } + serde_json::to_string(&repr).map(|s| println!("{s},")) + .map_err(|err| error!("{err}")) + .ok(); + }, + } + }, + Err(err) => error!("{err}"), + } + } + }) + .await; + if matches!(format, cli::Format::Json) { + println!("]"); + } + Ok(()) +} + + +/// [[mount::unmount_and]] with report. +#[cfg_attr(feature = "tracing", tracing::instrument())] +async fn unmount(query: Query, wait: bool, format: cli::Format) -> Result<(), error::Error> { + let results: Vec<_> = mount::unmount_and(query, wait).await?.collect().await; + for (i, res) in results.into_iter().enumerate() { + match res { + Ok(dev) => { + let repr = dev.as_report_short(); + match format { + cli::Format::Human => println!("{}", repr.to_printable_line()), + cli::Format::Json => { + if i > 0 { + println!(","); + } + serde_json::to_string(&repr).map(|s| println!("{s},")) + .map_err(|err| error!("{err}")) + .ok(); + }, + }; + }, + Err(err) => error!("{err}"), + } + } + Ok(()) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +async fn send(query: Query, + command: device::command::Command, + read: bool, + _format: cli::Format) + -> Result<(), error::Error> { + let senders = send::send_to_interfaces(query, command).await?; + + senders.for_each_concurrent(None, |res| { + async move { + if read { + match res { + Ok(mut interface) => usb::io::redirect_interface_to_stdout(&mut interface) + .inspect_ok(|_| trace!("Read interface done.")).await, + Err(err) => Err(err), + } + } else { + res.map(|_| ()) + } + }.inspect_err(|err| error!("{err}")) + .map(|_| ()) + }) + .await; + Ok(()) +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +async fn read(query: Query) -> Result<(), error::Error> { + let by_dev = |mut device: device::Device| -> Result<_, Error> { + device.info().serial_number().map(|s| trace!("reading {s}")); + let fut = async move { usb::io::redirect_to_stdout(&mut device).await }; + Ok(fut) + }; + + let by_sn = |sn: SerialNumber| -> Result<_, Error> { + let device = usb::discover::device(&sn)?; + by_dev(device) + }; + + + let by_port = |port: String| -> Result<_, Error> { + let fut = async move { + if let Err(err) = serial::dev_with_port(&port).map_ok(by_dev).await??.await { + warn!("Unable to map specified port {port} to device: {err}"); + serial::redirect_to_stdout(port).await?; + } + Ok(()) + }; + Ok(fut) + }; + + match query.value { + Some(Value::Serial(sn)) => by_sn(sn)?.await, + Some(Value::Path(port)) => by_port(port.to_string_lossy().to_string())?.await, + Some(Value::Com(port)) => by_port(format!("COM{port}"))?.await, + None => { + let mut devices: Vec<_> = usb::discover::devices_data()?.collect(); + match devices.len() { + 1 => by_dev(devices.remove(0))?.await, + 0 => Err(Error::not_found()), + len => { + error!("Read multiple devices not supported, plz connect exact one or specify its serial number. Found {len} devices."); + Err(Error::not_found()) + }, + } }, + } +} + + +#[cfg_attr(feature = "tracing", tracing::instrument())] +async fn list(format: cli::Format, kind: cli::DeviceKind) -> Result<(), error::Error> { + use mount::volume::volumes_for_map; + + let devices = match kind { + cli::DeviceKind::Any => volumes_for_map(usb::discover::devices()?).await?, + cli::DeviceKind::Storage => volumes_for_map(usb::discover::devices_storage()?).await?, + cli::DeviceKind::Data => usb::discover::devices_data()?.map(|dev| (dev, None)).collect(), + }.into_iter() + .map(|(dev, vol)| (dev, vol.map(|v| v.path().to_path_buf()))); - cli::Command::Mount(cfg) => mount::mount(&cfg)?.unmount_on_drop(false), + match format { + cli::Format::Human => { + for (mut dev, vol) in devices { + if !dev.is_ready() { + dev.open().ok(); + } - cli::Command::Install(cfg) => { - let mut path = install::install(&cfg)?; - path.device.refresh().ok(); - info!( - "Installed to {}, on-device path: {}", - path.device, - path.path.display() - ); + let vol = vol.map(|v| v.into()); + let repr = report::DevInfo::new(&dev, vol); + println!("{}", repr.to_printable_line()); + dev.close(); + } }, + cli::Format::Json => { + print!("["); + let devices: Vec<_> = devices.collect(); + let len = devices.len(); + for (i, (mut dev, vol)) in devices.into_iter().enumerate() { + if !dev.is_ready() { + dev.open().ok(); + } - cli::Command::Run(cfg) => run::run(cfg)?, + let vol = vol.map(|v| v.into()); + let repr = report::DevInfo::new(&dev, vol); + let repr = serde_json::to_string(&repr)?; + println!("{repr}"); + dev.close(); - cli::Command::Read(cfg) => { - let device = find_one_device(cfg.device)?; - debug!("device: {device:#?}"); - device.read_to_stdout(cfg.echo)?; + if i != len - 1 { + print!(", "); + } + } + println!("]"); }, } diff --git a/support/tool/src/model/mod.rs b/support/tool/src/model/mod.rs deleted file mode 100644 index 69324f56..00000000 --- a/support/tool/src/model/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::path::PathBuf; - -mod serial; -pub use serial::*; - - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Mode { - /// DATA / COMM - Data, - /// MASS_STORAGE - Storage, -} - -impl std::fmt::Display for Mode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { - Mode::Data => "D", - Mode::Storage => "S", - }) - } -} - - -pub struct Device { - pub(crate) serial: SerialNumber, - - /// Detected interface - pub(crate) mode: Mode, - - /// Mount point - pub(crate) volume: Option, - - /// Path of fd, cu or tty - pub(crate) tty: Option, -} - -impl std::fmt::Debug for Device { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut res = f.debug_struct(&self.serial.to_string()); - res.field("mode", &self.mode); - res.field("dev", &self.tty); - res.field("path", &self.volume); - - match &self.mode { - Mode::Storage => res.field("mounted", &self.mounted()), - Mode::Data => &mut res, - }; - - res.finish() - } -} - -impl std::fmt::Display for Device { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}({})", self.serial, self.mode) - } -} - - -struct DeviceFd { - cu: PathBuf, -} - -impl std::fmt::Debug for DeviceFd { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut res = f.debug_struct("Fd"); - res.field("cu", &self.cu); - res.finish() - } -} - -impl std::fmt::Display for DeviceFd { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.cu.display()) } -} diff --git a/support/tool/src/mount.rs b/support/tool/src/mount.rs deleted file mode 100644 index a228a528..00000000 --- a/support/tool/src/mount.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; - -use ::build::toolchain::sdk::Sdk; - -use crate::Error; -use crate::model::Device; -use crate::model::Mode; - - -#[cfg(target_os = "macos")] -pub const DEVICE_MOUNT_POINT_DEF: &str = "/Volumes/PLAYDATE"; -#[cfg(all(unix, not(target_os = "macos")))] -pub const DEVICE_MOUNT_POINT_DEF: &str = "/run/media/${USER}/PLAYDATE"; -#[cfg(not(unix))] -// FIXME: set the real expected path -pub const DEVICE_MOUNT_POINT_DEF: &str = "/TODO/PLAYDATE"; - - -impl Device { - pub fn mounted(&self) -> bool { - self.mode == Mode::Storage && - self.volume - .as_deref() - .filter(|p| p.try_exists().ok().unwrap_or_default()) - .is_some() - } -} - - -pub fn mount_point(mount: &Path) -> Result { - use ::build::assets::resolver::EnvResolver; - - let path = mount.to_str() - .ok_or_else(|| Error::Error(format!("Mount point is not invalid utf-8: {}", mount.display()))) - .map(|s| EnvResolver::new().str_only(s))?; - - let path: &Path = Path::new(path.as_ref()); - - if !path.try_exists()? { - Ok(path.to_path_buf()) - } else { - use std::io::{Error as IoError, ErrorKind}; - Err(IoError::new( - ErrorKind::AlreadyExists, - format!("Mount point is already exists: {}", mount.display()), - ).into()) - } -} - - -/// Mount `device` and return `MountHandle` with the given `mount`. -/// `mount` if __expected__ mount-point. -pub fn mount_device(device: &Device, mount: Option) -> Result { - let mount = if let Some(mount) = mount { - mount - } else { - mount_point(Path::new(DEVICE_MOUNT_POINT_DEF))? - }; - - // mount the device: - let mount_handle = if let Some(sdk) = Sdk::try_new().map_err(|err| error!("{err}")).ok() { - mount_with(&sdk, &mount, &device) - } else { - mount_without_sdk(&mount, &device) - }?; - - info!( - "{device} successfully mounted to {}", - mount_handle.path().display() - ); - Ok(mount_handle) -} - - -/// Send command to `device` and return `MountHandle` with the given `mount`. -/// A `mount` is __expected__ mount-point. -pub fn mount_with(sdk: &Sdk, mount: &Path, device: &Device) -> Result { - let mount = mount_point(mount)?; - trace!("serial: {device:?}, mount to: {}", mount.display()); - - ensure_device_mountable(device)?; - let tty = device.tty.as_deref().ok_or(Error::unable_to_find_tty(device))?; - let mut cmd = Command::new(sdk.pdutil()); - cmd.arg(tty).arg("datadisk"); - debug!("Run: {:?}", cmd); - cmd.status()?.exit_ok()?; - - Ok(MountHandle::new(mount)) -} - -pub fn mount_by_device_tty_with(sdk: &Sdk, mount: &Path, device_tty: &Path) -> Result { - let mount = mount_point(mount)?; - trace!("tty: {}, mount to: {}", device_tty.display(), mount.display()); - - let mut cmd = Command::new(sdk.pdutil()); - cmd.arg(device_tty).arg("datadisk"); - debug!("Run: {:?}", cmd); - cmd.status()?.exit_ok()?; - - Ok(MountHandle::new(mount)) -} - - -pub fn mount_without_sdk(mount: &Path, device: &Device) -> Result { - ensure_device_mountable(device)?; - send_storage_mode_to_device_without_sdk(mount, device).map_err(|err| { - Error::Error(format!("Unable to send command to {device:?}: {err}.")) - })?; - Ok(MountHandle::new(mount.to_path_buf())) -} - -pub fn send_storage_mode_to_device_without_sdk(mount: &Path, device: &Device) -> Result { - device.write("datadisk").map_err(|err| { - error!("{err}"); - Error::Error(format!("Unable to send command to {device}.")) - })?; - Ok(MountHandle::new(mount.to_path_buf())) -} - - -pub fn ensure_device_mountable(device: &Device) -> Result<(), Error> { - if !device.mounted() { - Ok(()) - } else { - let volume = device.volume - .as_deref() - .map(|p| format!(" at {}", p.display())) - .unwrap_or_default(); - Err(Error::Error(format!("{device} is already mounted{}", volume))) - } -} - - -pub struct MountHandle { - path: PathBuf, - unmount_on_drop: bool, -} - -impl MountHandle { - pub fn new(path: PathBuf) -> Self { - MountHandle { path, - unmount_on_drop: true } - } - - pub fn unmount_on_drop(&mut self, value: bool) { self.unmount_on_drop = value; } - pub fn path(&self) -> &Path { &self.path } - - pub(crate) fn set_mount_point(&mut self, path: PathBuf) { self.path = path; } -} - -impl Drop for MountHandle { - fn drop(&mut self) { - if self.unmount_on_drop { - debug!("Unmounting {}", self.path.display()); - let _ = unmount(&self.path).map_err(|err| { - error!("{err}"); - info!("Please press 'A' on the Playdate to exit Data Disk mode."); - }) - .ok(); - } - } -} - - -#[cfg(target_os = "macos")] -pub fn unmount(path: &Path) -> Result<(), Error> { - Command::new("diskutil").arg("eject") - .arg(path) - .status()? - .exit_ok()?; - Ok(()) -} - -#[cfg(target_os = "linux")] -pub fn unmount(path: &Path) -> Result<(), Error> { - Command::new("eject").arg(path).status()?.exit_ok()?; - Ok(()) -} - -#[cfg(target_os = "windows")] -pub fn unmount(path: &Path) -> Result<(), Error> { - warn!("Unmounting not implemented for windows yet."); - Command::new("eject").arg(path).status()?.exit_ok()?; - Ok(()) -} diff --git a/support/tool/src/report.rs b/support/tool/src/report.rs new file mode 100644 index 00000000..5ac3783b --- /dev/null +++ b/support/tool/src/report.rs @@ -0,0 +1,140 @@ +use std::borrow::Cow; +use std::path::Path; + +use pddev::{device, interface, usb, mount}; +use pddev::usb::mode::DeviceMode; + + +pub trait AsReport { + #[allow(unused)] + fn as_report(&self) -> DevInfo<'_>; + fn as_report_short(&self) -> DevInfoShort<'_>; +} + + +impl AsReport for device::Device { + fn as_report(&self) -> DevInfo<'_> { DevInfo::new(self, None) } + fn as_report_short(&self) -> DevInfoShort<'_> { DevInfoShort::new(self, None) } +} + +impl AsReport for mount::MountedDevice { + fn as_report(&self) -> DevInfo<'_> { DevInfo::new(&self.device, Some(self.handle.path())) } + fn as_report_short(&self) -> DevInfoShort<'_> { DevInfoShort::new(&self.device, Some(self.handle.path())) } +} + + +#[derive(Debug, serde::Serialize)] +pub struct DevInfoShort<'dev> { + #[serde(skip_serializing_if = "Option::is_none")] + serial: Option<&'dev str>, + #[serde(flatten)] + state: DevState<'dev>, +} + +impl<'dev> DevInfoShort<'dev> { + fn new(dev: &'dev device::Device, vol: Option>) -> Self { + Self { serial: dev.info().serial_number(), + state: DevState::new(dev, vol) } + } + + pub fn to_printable_line(&self) -> impl std::fmt::Display { + let mut s = String::new(); + self.to_printable_line_to(&mut s); + s + } + + pub fn to_printable_line_to(&self, buf: &mut String) { + if let Some(serial) = self.serial { + buf.push_str(serial); + buf.push(' '); + } + self.state.to_printable_line_to(buf); + } +} + +#[derive(Debug, serde::Serialize)] +pub struct DevInfo<'dev> { + address: u8, + #[serde(skip_serializing_if = "Option::is_none")] + product: Option<&'dev str>, + #[serde(skip_serializing_if = "Option::is_none")] + manufacturer: Option<&'dev str>, + #[serde(flatten)] + inner: DevInfoShort<'dev>, +} + +impl<'dev> DevInfo<'dev> { + pub fn new(dev: &'dev device::Device, vol: Option>) -> Self { + Self { address: dev.info().device_address(), + product: dev.info().product_string(), + manufacturer: dev.info().manufacturer_string(), + inner: DevInfoShort::new(dev, vol) } + } + + pub fn to_printable_line(&self) -> impl std::fmt::Display { + let mut s = format!("{:02x} ", self.address); + if let Some(product) = self.product { + s.push_str(product); + s.push(' '); + } + if let Some(manufacturer) = self.manufacturer { + s.push_str(manufacturer); + s.push(' '); + } + s.push_str(&self.inner.to_printable_line().to_string()); + s + } +} + + +#[derive(Debug, serde::Serialize)] +pub struct DevState<'dev> { + mode: Cow<'dev, str>, + #[serde(skip_serializing_if = "Option::is_none")] + volume: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + interface: Option>, +} + +impl<'dev> DevState<'dev> { + pub fn new(dev: &'dev device::Device, vol: Option>) -> Self { + let interface = interface_repr(dev); + Self { mode: dev.mode().to_string().into(), + volume: vol, + interface } + } + + #[allow(unused)] + pub fn to_printable_line(&self) -> impl std::fmt::Display { + let mut s = String::new(); + self.to_printable_line_to(&mut s); + s + } + + pub fn to_printable_line_to(&self, buf: &mut String) { + buf.push_str(&format!("({})", self.mode)); + + if let Some(ref volume) = self.volume { + buf.push(' '); + buf.push_str(&volume.to_string_lossy()); + } + if let Some(ref interface) = self.interface { + buf.push(' '); + buf.push_str(interface); + } + } +} + + +fn interface_repr(dev: &device::Device) -> Option> { + let interface = if matches!(dev.mode(), usb::mode::Mode::Data) { + match dev.interface().ok()? { + interface::Interface::Usb(_) => Some(Cow::from("bulk")), + interface::Interface::Serial(dev) => Some(Cow::from(dev.info().port_name.to_owned())), + } + } else { + None + }; + + interface +} diff --git a/support/tool/src/search/fs.rs b/support/tool/src/search/fs.rs deleted file mode 100644 index fb7b6856..00000000 --- a/support/tool/src/search/fs.rs +++ /dev/null @@ -1,35 +0,0 @@ -use std::path::Path; - -use crate::model::Device; -use crate::model::Mode; - - -pub fn is_tty_fd(path: &Path) -> Result { - let res = if path.components().count() > 1 && path.try_exists()? { - #[cfg(unix)] - { - use std::os::unix::fs::FileTypeExt; - let fty = path.metadata()?.file_type(); - fty.is_char_device() || fty.is_block_device() || fty.is_fifo() || fty.is_socket() - } - #[cfg(not(unix))] - // TODO: check - true - } else { - false - }; - - Ok(res) -} - - -impl Device { - // TODO: rewrite to `is_data_available()` where check usb (if feature=usb) and tty availability. - pub fn is_cu_ok(&self) -> bool { - self.mode == Mode::Data && - self.tty - .as_deref() - .filter(|p| p.try_exists().ok().unwrap_or_default()) - .is_some() - } -} diff --git a/support/tool/src/search/mac.rs b/support/tool/src/search/mac.rs deleted file mode 100644 index d9980f32..00000000 --- a/support/tool/src/search/mac.rs +++ /dev/null @@ -1,239 +0,0 @@ -#![cfg(target_os = "macos")] -use std::process::Command; -use std::path::Path; -use std::path::PathBuf; -use serde::Deserialize; - -use crate::Error; -use crate::model::Mode; -use crate::model::Device; - - -/// Get a list of all available devices by OS -pub fn list_usb_devices() -> Result, Error> { - let mut devices = system_profiler_spusbdata().map_err(|err| debug!("{err}")) - .unwrap_or_default(); - let mut tty = tty::find_cu_devices()?.collect::>(); - - // merge: - tty.drain(..).for_each(|tty| { - if let Some(device) = devices.iter_mut().find(|d| d.serial == tty.serial) { - device.refresh_from(tty); - } else { - devices.push(tty); - } - }); - - Ok(devices) -} - - -impl Device { - pub fn refresh(&mut self) -> Result<(), Error> { - let devices = system_profiler_spusbdata()?; - let device = devices.into_iter() - .filter(|d| d.serial == self.serial) - .next() - .ok_or("No device found")?; - self.refresh_from(device); - self.refresh_tty()?; - Ok(()) - } - - pub fn refresh_tty(&mut self) -> Result<(), Error> { - self.tty = tty::find_cu_for(&self.serial)?; - Ok(()) - } -} - - -/// Get a list of all available devices by OS -/// and update the `devices`. -pub fn refresh(devices: &mut [Device]) -> Result<(), Error> { - list_usb_devices()?.into_iter().for_each(|a| { - devices.iter_mut() - .find(|b| b.serial == a.serial) - .map(|b| b.refresh_from(a)); - }); - Ok(()) -} - - -mod tty { - use std::path::PathBuf; - use crate::Error; - use crate::model::Device; - use crate::model::Mode; - use crate::model::SerialNumber; - - - const DIR: &str = "/dev"; - - /// Search for a cu fd that looks like to Playdate. - pub fn find_cu_devices() -> Result, Error> { - let devices = std::fs::read_dir(DIR)?.filter_map(move |entry| { - let entry = entry.ok()?; - let name = entry.file_name(); - let name = name.to_string_lossy(); - let serial: SerialNumber = name.parse().ok()?; - if name.starts_with("cu.usbmodem") { - let cu = entry.path(); - Some(Device { serial, - mode: Mode::Data, - tty: Some(cu), - volume: None }) - } else { - None - } - }); - Ok(devices) - } - - - pub fn find_cu_for(serial: &SerialNumber) -> Result, Error> { - Ok(std::fs::read_dir(DIR)?.filter_map(move |entry| { - let entry = entry.ok()?; - let name = entry.file_name(); - let name = name.to_string_lossy(); - if name.starts_with("cu.usbmodem") && - &SerialNumber::try_from(name.as_ref()).ok()? == serial - { - Some(entry.path()) - } else { - None - } - }) - .next()) - } -} - - -/// Call `system_profiler -json SPUSBDataType` -fn system_profiler_spusbdata() -> Result, Error> { - let output = Command::new("system_profiler").args(["-json", "SPUSBDataType"]) - .output()?; - output.status.exit_ok()?; - - let data: SystemProfilerResponse = serde_json::from_reader(&output.stdout[..])?; - - let result = data.data - .into_iter() - .filter_map(|c| c.items) - .flatten() - .filter(|item| item.vendor_id == "0x1331") - .filter_map(|item| { - let name = item.name.to_owned(); - let serial = item.serial_num.to_owned(); - device_info_to_device(item).map_err(|err| { - debug!("{} {} {:?}", name, serial, err); - }) - .ok() - }) - .collect::>(); - - - Ok(result) -} - -fn device_info_to_device(info: DeviceInfo) -> Result { - let serial = info.serial_num.parse()?; - let mode = if info.media.is_some() { - Mode::Storage - } else { - Mode::Data - }; - let volume = info.media - .map(|media| { - media.into_iter() - .flat_map(|root| root.volumes.into_iter()) - .filter_map(|par| { - if let Some(path) = par.mount_point { - Some(path) - } else { - let path = Path::new("/Volumes").join(&par.name); - if path.try_exists().ok()? { - Some(path) - } else { - mount_point_for_partition(&par).map_err(|err| { - debug!("{err:?}"); - }) - .ok() - } - } - }) - .next() - }) - .flatten(); - - Ok(Device { serial, - mode, - volume, - tty: None }) -} - -/// Calls `diskutil info -plist {partition.volume_uuid}` -fn mount_point_for_partition(par: &MediaPartitionInfo) -> Result { - let output = Command::new("diskutil").args(["info", "-plist"]) - .arg(&par.volume_uuid) - .output()?; - output.status.exit_ok()?; - let info: DiskUtilResponse = plist::from_bytes(output.stdout.as_slice())?; - info.mount_point - .ok_or(format!( - "Mount point not found for partition {} {}", - &par.name, &par.bsd_name - ).into()) - .map(PathBuf::from) -} - - -#[derive(Deserialize, Debug)] -struct DiskUtilResponse { - // #[serde(rename = "Ejectable")] - // ejectable: bool, - #[serde(rename = "MountPoint")] - mount_point: Option, -} - - -#[derive(Deserialize, Debug)] -struct SystemProfilerResponse { - #[serde(rename = "SPUSBDataType")] - data: Vec, -} - - -#[derive(Deserialize, Debug)] -struct ControllerInfo { - #[serde(rename = "_items")] - items: Option>, -} - -#[derive(Deserialize, Debug)] -#[allow(dead_code)] -struct DeviceInfo { - #[serde(rename = "_name")] - name: String, - manufacturer: String, - product_id: String, - serial_num: String, - vendor_id: String, - - #[serde(rename = "Media")] - media: Option>, -} - - -#[derive(Deserialize, Debug)] -struct DeviceMediaInfo { - volumes: Vec, -} - -#[derive(Deserialize, Debug)] -struct MediaPartitionInfo { - #[serde(rename = "_name")] - name: String, - bsd_name: String, - volume_uuid: String, - mount_point: Option, -} diff --git a/support/tool/src/search/mod.rs b/support/tool/src/search/mod.rs deleted file mode 100644 index a495a6dd..00000000 --- a/support/tool/src/search/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::model::Device; - -pub mod fs; -mod mac; -mod unix; -mod other; -mod uni { - #[cfg(target_os = "macos")] - pub use super::mac::*; - #[cfg(all(unix, not(target_os = "macos")))] - pub use super::unix::*; - #[cfg(not(unix))] - pub use super::other::*; -} - - -pub fn list_connected_devices() -> Result, crate::Error> { - #[cfg(feature = "usb")] - let devices = { - let mut devices = crate::usb::list_usb_devices()?; - uni::refresh(&mut devices[..])?; - devices - }; - #[cfg(not(feature = "usb"))] - let devices = uni::list_usb_devices()?; - - trace!("discovered devices: {devices:#?}"); - Ok(devices) -} - - -impl Device { - fn refresh_from(&mut self, other: Self) { - if self.mode != other.mode { - debug!("{}: mode changed: {} <- {}", other.serial, other.mode, self.mode); - self.mode = other.mode; - } - - self.serial = other.serial; - if other.volume.is_some() { - self.volume = other.volume; - } - if other.tty.is_some() { - self.tty = other.tty; - } - } -} diff --git a/support/tool/src/search/other.rs b/support/tool/src/search/other.rs deleted file mode 100644 index 03fd6532..00000000 --- a/support/tool/src/search/other.rs +++ /dev/null @@ -1,27 +0,0 @@ -#![cfg(not(unix))] -#[cfg(not(feature = "usb"))] -compile_error!("Currently unsupported platform, please try build with feature `usb`."); - - -use crate::Error; -use crate::model::Mode; -use crate::model::Device; - - -pub fn list_usb_devices() -> Result, Error> { Ok(vec![]) } - -/// Get a list of all available devices by OS -/// and update the `devices`. -pub fn refresh(devices: &mut [Device]) -> Result<(), Error> { Ok(()) } - -impl Device { - pub fn refresh(&mut self) -> Result<(), Error> { - // XXX: I have no idea how it works on windows and other non-unix systems. - Ok(()) - } - - pub fn refresh_tty(&mut self) -> Result<(), Error> { - // XXX: I have no idea how it works on windows and other non-unix systems. - Ok(()) - } -} diff --git a/support/tool/src/search/unix.rs b/support/tool/src/search/unix.rs deleted file mode 100644 index 925cddda..00000000 --- a/support/tool/src/search/unix.rs +++ /dev/null @@ -1,77 +0,0 @@ -#![cfg(all(unix, not(target_os = "macos")))] -use crate::Error; -use crate::model::Device; - - -/// Get a list of all available devices by OS -pub fn list_usb_devices() -> Result, Error> { Ok(tty::find_tty_devices()?.collect::>()) } - - -impl Device { - pub fn refresh(&mut self) -> Result<(), Error> { self.refresh_tty() } - - pub fn refresh_tty(&mut self) -> Result<(), Error> { - self.tty = tty::find_tty_for(&self.serial)?; - Ok(()) - } -} - - -/// Get a list of all available devices by OS -/// and update the `devices`. -pub fn refresh(devices: &mut [Device]) -> Result<(), Error> { - list_usb_devices()?.into_iter().for_each(|a| { - devices.iter_mut() - .find(|b| b.serial == a.serial) - .map(|b| b.refresh_from(a)); - }); - Ok(()) -} - - -mod tty { - use std::path::PathBuf; - - use regex::Regex; - use crate::Error; - use crate::model::Device; - use crate::model::Mode; - use crate::model::SerialNumber; - - - /// Search for a cu fd that looks like to Playdate. - pub fn find_tty_devices() -> Result, Error> { - // TODO: fix this, use check like for mac: prefix + `SerialNumber::try_from` instead of this regex: - let re = Regex::new(r"^usb-Panic_Inc_Playdate_(PDU\d+[_-].+)$").expect("invalid regex"); - let devices = - std::fs::read_dir("/dev/serial/by-id")?.filter_map(move |entry| { - let entry = entry.ok()?; - let name = entry.file_name(); - let name_lossy = name.to_string_lossy(); - let captures = re.captures(name_lossy.as_ref())?; - let _ = captures.get(1)?.as_str(); - let path = entry.path().canonicalize().ok()?; - if path.to_string_lossy().contains("tty") { - Some(Device { serial: name_lossy.as_ref().parse().ok()?, - tty: Some(path), - mode: Mode::Data, - volume: None }) - } else { - None - } - }); - // TODO: warn errors and outfiltered - Ok(devices) - } - - pub fn find_tty_for(serial: &SerialNumber) -> Result, Error> { - Ok(find_tty_devices()?.filter_map(move |device| { - if &device.serial == serial { - device.tty - } else { - None - } - }) - .next()) - } -} diff --git a/support/tool/src/usb/io.rs b/support/tool/src/usb/io.rs deleted file mode 100644 index 5faa93d2..00000000 --- a/support/tool/src/usb/io.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::time::Duration; -use rusb::{Device, DeviceDescriptor, DeviceHandle, Direction, Result, TransferType, UsbContext}; - -#[derive(Debug)] -struct Endpoint { - config: u8, - iface: u8, - setting: u8, - address: u8, -} - -// TODO: impl real IO mode for ping for example: -// - buffered read, write -// - within one non-global context - - -pub fn read_device(device: &mut Device, - device_desc: &DeviceDescriptor, - handle: &mut DeviceHandle) - -> Result<()> { - debug!("USB: Start reading device..."); - handle.reset()?; - - match find_readable_endpoint(device, device_desc, TransferType::Interrupt) { - Some(endpoint) => read_endpoint(handle, endpoint, TransferType::Interrupt), - None => debug!("No readable interrupt endpoint"), - } - - match find_readable_endpoint(device, device_desc, TransferType::Bulk) { - Some(endpoint) => read_endpoint(handle, endpoint, TransferType::Bulk), - None => debug!("No readable bulk endpoint"), - } - - Ok(()) -} - -pub fn write_device>(device: &mut Device, - device_desc: &DeviceDescriptor, - handle: &mut DeviceHandle, - command: S) - -> Result<()> { - debug!("USB: Start writing to device '{}'...", command.as_ref()); - handle.reset()?; - - match find_writable_endpoint(device, device_desc, TransferType::Interrupt) { - Some(endpoint) => { - write_endpoint( - handle, - endpoint, - TransferType::Interrupt, - command.as_ref().as_bytes(), - ) - }, - None => debug!("No writable interrupt endpoint"), - } - - match find_writable_endpoint(device, device_desc, TransferType::Bulk) { - Some(endpoint) => write_endpoint(handle, endpoint, TransferType::Bulk, command.as_ref().as_bytes()), - None => debug!("No writable bulk endpoint"), - } - - Ok(()) -} - -fn find_readable_endpoint(device: &mut Device, - device_desc: &DeviceDescriptor, - transfer_type: TransferType) - -> Option { - for n in 0..device_desc.num_configurations() { - let config_desc = match device.config_descriptor(n) { - Ok(c) => c, - Err(_) => continue, - }; - - for interface in config_desc.interfaces() { - for interface_desc in interface.descriptors() { - for endpoint_desc in interface_desc.endpoint_descriptors() { - if endpoint_desc.direction() == Direction::In && endpoint_desc.transfer_type() == transfer_type { - return Some(Endpoint { config: config_desc.number(), - iface: interface_desc.interface_number(), - setting: interface_desc.setting_number(), - address: endpoint_desc.address() }); - } - } - } - } - } - - None -} - -fn find_writable_endpoint(device: &mut Device, - device_desc: &DeviceDescriptor, - transfer_type: TransferType) - -> Option { - for n in 0..device_desc.num_configurations() { - let config_desc = match device.config_descriptor(n) { - Ok(c) => c, - Err(_) => continue, - }; - - for interface in config_desc.interfaces() { - for interface_desc in interface.descriptors() { - for endpoint_desc in interface_desc.endpoint_descriptors() { - if endpoint_desc.direction() == Direction::Out && endpoint_desc.transfer_type() == transfer_type { - return Some(Endpoint { config: config_desc.number(), - iface: interface_desc.interface_number(), - setting: interface_desc.setting_number(), - address: endpoint_desc.address() }); - } - } - } - } - } - - None -} - -fn read_endpoint(handle: &mut DeviceHandle, endpoint: Endpoint, transfer_type: TransferType) { - debug!("Reading from endpoint: {:?}", endpoint); - - let has_kernel_driver = match handle.kernel_driver_active(endpoint.iface) { - Ok(true) => { - handle.detach_kernel_driver(endpoint.iface).ok(); - true - }, - _ => false, - }; - debug!(" kernel driver: {}", has_kernel_driver); - - match configure_endpoint(handle, &endpoint) { - Ok(_) => { - let mut buf = [0; 256]; - let timeout = Duration::from_secs(60); - - debug!(" read while..."); - let mut ok = true; - while ok { - let res = match transfer_type { - TransferType::Interrupt => handle.read_interrupt(endpoint.address, &mut buf, timeout), - TransferType::Bulk => handle.read_bulk(endpoint.address, &mut buf, timeout), - _ => unreachable!(), - }; - ok = res.is_ok(); - match res { - Ok(len) => { - use std::io::prelude::*; - std::io::stdout().write_all(&buf[..len]).ok(); - }, - Err(err) => error!("could not read from endpoint: {}", err), - } - } - }, - Err(err) => error!("could not configure endpoint: {}", err), - } - - if has_kernel_driver { - handle.attach_kernel_driver(endpoint.iface).ok(); - } -} - - -fn write_endpoint(handle: &mut DeviceHandle, - endpoint: Endpoint, - transfer_type: TransferType, - buf: &[u8]) { - debug!("Writing from endpoint: {:?}", endpoint); - - let has_kernel_driver = match handle.kernel_driver_active(endpoint.iface) { - Ok(true) => { - handle.detach_kernel_driver(endpoint.iface).ok(); - true - }, - _ => false, - }; - debug!(" kernel driver: {}", has_kernel_driver); - - match configure_endpoint(handle, &endpoint) { - Ok(_) => { - let timeout = Duration::from_secs(60 * 4); - - debug!(" write {buf:?}"); - let res = match transfer_type { - TransferType::Interrupt => handle.write_interrupt(endpoint.address, &buf, timeout), - TransferType::Bulk => handle.write_bulk(endpoint.address, &buf, timeout), - _ => unreachable!(), - }; - match res { - Ok(len) => debug!(" wrote {len} bytes"), - Err(err) => error!("could not read from endpoint: {}", err), - } - }, - Err(err) => error!("could not configure endpoint: {}", err), - } - - if has_kernel_driver { - handle.attach_kernel_driver(endpoint.iface).ok(); - } -} - -fn configure_endpoint(handle: &mut DeviceHandle, endpoint: &Endpoint) -> Result<()> { - handle.set_active_configuration(endpoint.config)?; - handle.claim_interface(endpoint.iface)?; - handle.set_alternate_setting(endpoint.iface, endpoint.setting)?; - Ok(()) -} diff --git a/support/tool/src/usb/mod.rs b/support/tool/src/usb/mod.rs deleted file mode 100644 index a0d62b26..00000000 --- a/support/tool/src/usb/mod.rs +++ /dev/null @@ -1,382 +0,0 @@ -#![cfg(feature = "usb")] -use std::borrow::Cow; - -use crate::Error; -use crate::model::Device; -use crate::model::Mode; -use crate::model::SerialNumber; - -mod io; - - -pub fn list_usb_devices() -> Result, Error> { - let mut found = Vec::new(); - - #[cfg(debug_assertions)] - debug::list_devices()?; - - for device in rusb::devices()?.iter().into_iter() { - match usb_to_pd(&device) { - Ok(Some(pd)) => found.push(pd), - Err(err) => debug!("{err}"), - Ok(None) => continue, - } - } - Ok(found) -} - - -fn usb_to_pd(device: &rusb::Device) -> Result, Error> { - use rusb::constants::{LIBUSB_CLASS_COMM, LIBUSB_CLASS_DATA, LIBUSB_CLASS_MASS_STORAGE}; - - let descriptor = device.device_descriptor()?; - if descriptor.vendor_id() != 0x1331 { - return Ok(None); - } - - // Probably not need to filter product id because revB, etc.. - // if descriptor.product_id() != 0x5740 { continue } - - let handle = device.open()?; - let serial = handle.read_serial_number_string_ascii(&descriptor)?; - let serial = SerialNumber::try_from(serial)?; - // let man = handle.read_manufacturer_string_ascii(&descriptor)?; - // if !man.contains("Panic") { continue; } - - // get pd device mode: - let cfg_id = handle.active_configuration()?; - let config_descriptor = device.config_descriptor(cfg_id - 1)?; - let modes = - config_descriptor.interfaces() - .flat_map(|interface| { - interface.descriptors().filter_map(|d| { - let class = d.class_code(); - match class { - LIBUSB_CLASS_MASS_STORAGE => Some(Mode::Storage), - LIBUSB_CLASS_DATA | LIBUSB_CLASS_COMM => Some(Mode::Data), - _ => None, - } - }) - }) - .collect::>(); - let mode = if modes.contains(&Mode::Storage) { - Mode::Storage - } else if modes.contains(&Mode::Data) { - Mode::Data - } else { - return Err(Error::Err("Unknown device mode, required interfaces not found")); - }; - - - Ok(Some(Device { serial, - mode, - volume: None, - tty: None })) -} - - -pub fn read_output(device: &Device, echo: Option) -> Result<(), Error> { - let (mut handle, _) = open(&device)?; - let mut dev = handle.device(); - let descr = dev.device_descriptor()?; - - if let Some(echo) = echo { - let v = if echo { "on" } else { "off" }; - io::write_device(&mut dev, &descr, &mut handle, format!("echo {v}"))?; - } - io::read_device(&mut dev, &descr, &mut handle)?; - - Ok(()) -} - -pub fn write>(device: &Device, command: S) -> Result<(), Error> { - let (mut handle, _) = open(&device)?; - let mut dev = handle.device(); - let descr = dev.device_descriptor()?; - - let command: Cow = if command.as_ref().ends_with("\n") { - command.as_ref().into() - } else { - format!("{}\n", command.as_ref()).into() - }; - - debug!("USB: write: '{}'", command.as_ref()); - - io::write_device(&mut dev, &descr, &mut handle, command)?; - - Ok(()) -} - - -pub fn open(device: &Device) -> Result<(rusb::DeviceHandle, Device), Error> { - let (handle, pd) = if let Some(handle) = rusb::open_device_with_vid_pid(0x1331, 0x5740) { - let pd = usb_to_pd(&handle.device()).ok() - .flatten() - .filter(|pd| pd.serial == device.serial && pd.mode == Mode::Data); - if let Some(pd) = pd { - Ok((handle, pd)) - } else if let Some((usb, dev)) = find(&device).ok().filter(|(_, d)| d.mode == Mode::Data) { - usb.open().map(|handle| (handle, dev)).map_err(Error::from) - } else { - Err(Error::named_device_not_found(device.serial.to_string())) - } - } else if let Some((usb, dev)) = find(&device).ok().filter(|(_, d)| d.mode == Mode::Data) { - usb.open().map(|handle| (handle, dev)).map_err(Error::from) - } else { - Err(Error::named_device_not_found(device.serial.to_string())) - }?; - Ok((handle, pd)) -} - - -// XXX: This is absolutely ineffective, optimize it. -/// Search and return usb device looks like a playdate and a our simple interface struct `Device`. -pub fn find(device: &Device) -> Result<(rusb::Device, Device), Error> { - let dev = rusb::devices()?.iter().into_iter().find_map(|dev| { - if let Some(pd) = usb_to_pd(&dev).ok().flatten() { - if pd.serial == device.serial { - Some((dev, pd)) - } else { - None - } - } else { - None - } - }); - dev.ok_or(Error::named_device_not_found(device.serial.to_string())) -} - - -pub mod debug { - use rusb::{ConfigDescriptor, DeviceDescriptor, DeviceHandle, DeviceList, EndpointDescriptor, InterfaceDescriptor, - Language, Result, Speed, UsbContext}; - use std::time::Duration; - use usb_ids::{self, FromId}; - - struct UsbDevice { - handle: DeviceHandle, - language: Language, - timeout: Duration, - } - - - pub fn list_devices() -> Result<()> { - let timeout = Duration::from_secs(1); - - for device in DeviceList::new()?.iter() { - let device_desc = match device.device_descriptor() { - Ok(d) => d, - Err(_) => continue, - }; - - let mut usb_device = { - match device.open() { - Ok(h) => { - match h.read_languages(timeout) { - Ok(l) => { - if !l.is_empty() { - Some(UsbDevice { handle: h, - language: l[0], - timeout, }) - } else { - None - } - }, - Err(_) => None, - } - }, - Err(_) => None, - } - }; - - println!( - "Bus {:03} Device {:03} ID {:04x}:{:04x} {}", - device.bus_number(), - device.address(), - device_desc.vendor_id(), - device_desc.product_id(), - get_speed(device.speed()) - ); - print_device(&device_desc, &mut usb_device); - - for n in 0..device_desc.num_configurations() { - let config_desc = match device.config_descriptor(n) { - Ok(c) => c, - Err(_) => continue, - }; - - print_config(&config_desc, &mut usb_device); - - for interface in config_desc.interfaces() { - for interface_desc in interface.descriptors() { - print_interface(&interface_desc, &mut usb_device); - - for endpoint_desc in interface_desc.endpoint_descriptors() { - print_endpoint(&endpoint_desc); - } - } - } - } - } - - Ok(()) - } - - fn print_device(device_desc: &DeviceDescriptor, handle: &mut Option>) { - let vid = device_desc.vendor_id(); - let pid = device_desc.product_id(); - - let vendor_name = match usb_ids::Vendor::from_id(device_desc.vendor_id()) { - Some(vendor) => vendor.name(), - None => "Unknown vendor", - }; - - let product_name = match usb_ids::Device::from_vid_pid(device_desc.vendor_id(), device_desc.product_id()) { - Some(product) => product.name(), - None => "Unknown product", - }; - - println!("Device Descriptor:"); - println!( - " bcdUSB {:2}.{}{}", - device_desc.usb_version().major(), - device_desc.usb_version().minor(), - device_desc.usb_version().sub_minor() - ); - println!(" bDeviceClass {:#04x}", device_desc.class_code()); - println!(" bDeviceSubClass {:#04x}", device_desc.sub_class_code()); - println!(" bDeviceProtocol {:#04x}", device_desc.protocol_code()); - println!(" bMaxPacketSize0 {:3}", device_desc.max_packet_size()); - println!(" idVendor {vid:#06x} {vendor_name}",); - println!(" idProduct {pid:#06x} {product_name}",); - println!( - " bcdDevice {:2}.{}{}", - device_desc.device_version().major(), - device_desc.device_version().minor(), - device_desc.device_version().sub_minor() - ); - println!( - " iManufacturer {:3} {}", - device_desc.manufacturer_string_index().unwrap_or(0), - handle.as_mut().map_or(String::new(), |h| { - h.handle - .read_manufacturer_string(h.language, device_desc, h.timeout) - .unwrap_or_default() - }) - ); - println!( - " iProduct {:3} {}", - device_desc.product_string_index().unwrap_or(0), - handle.as_mut().map_or(String::new(), |h| { - h.handle - .read_product_string(h.language, device_desc, h.timeout) - .unwrap_or_default() - }) - ); - println!( - " iSerialNumber {:3} {}", - device_desc.serial_number_string_index().unwrap_or(0), - handle.as_mut().map_or(String::new(), |h| { - h.handle - .read_serial_number_string(h.language, device_desc, h.timeout) - .unwrap_or_default() - }) - ); - println!(" bNumConfigurations {:3}", device_desc.num_configurations()); - } - - fn print_config(config_desc: &ConfigDescriptor, handle: &mut Option>) { - println!(" Config Descriptor:"); - println!(" bNumInterfaces {:3}", config_desc.num_interfaces()); - println!(" bConfigurationValue {:3}", config_desc.number()); - println!( - " iConfiguration {:3} {}", - config_desc.description_string_index().unwrap_or(0), - handle.as_mut().map_or(String::new(), |h| { - h.handle - .read_configuration_string(h.language, config_desc, h.timeout) - .unwrap_or_default() - }) - ); - println!(" bmAttributes:"); - println!(" Self Powered {:>5}", config_desc.self_powered()); - println!(" Remote Wakeup {:>5}", config_desc.remote_wakeup()); - println!(" bMaxPower {:4}mW", config_desc.max_power()); - - if !config_desc.extra().is_empty() { - println!(" {:?}", config_desc.extra()); - } else { - println!(" no extra data"); - } - } - - fn print_interface(interface_desc: &InterfaceDescriptor, handle: &mut Option>) { - println!(" Interface Descriptor:"); - println!( - " bInterfaceNumber {:3}", - interface_desc.interface_number() - ); - println!(" bAlternateSetting {:3}", interface_desc.setting_number()); - println!(" bNumEndpoints {:3}", interface_desc.num_endpoints()); - println!(" bInterfaceClass {:#04x}", interface_desc.class_code()); - println!( - " bInterfaceSubClass {:#04x}", - interface_desc.sub_class_code() - ); - println!( - " bInterfaceProtocol {:#04x}", - interface_desc.protocol_code() - ); - println!( - " iInterface {:3} {}", - interface_desc.description_string_index().unwrap_or(0), - handle.as_mut().map_or(String::new(), |h| { - h.handle - .read_interface_string(h.language, interface_desc, h.timeout) - .unwrap_or_default() - }) - ); - - if interface_desc.extra().is_empty() { - println!(" {:?}", interface_desc.extra()); - } else { - println!(" no extra data"); - } - } - - fn print_endpoint(endpoint_desc: &EndpointDescriptor) { - println!(" Endpoint Descriptor:"); - println!( - " bEndpointAddress {:#04x} EP {} {:?}", - endpoint_desc.address(), - endpoint_desc.number(), - endpoint_desc.direction() - ); - println!(" bmAttributes:"); - println!( - " Transfer Type {:?}", - endpoint_desc.transfer_type() - ); - println!(" Synch Type {:?}", endpoint_desc.sync_type()); - println!( - " Usage Type {:?}", - endpoint_desc.usage_type() - ); - println!( - " wMaxPacketSize {:#06x}", - endpoint_desc.max_packet_size() - ); - println!(" bInterval {:3}", endpoint_desc.interval()); - } - - fn get_speed(speed: Speed) -> &'static str { - match speed { - Speed::SuperPlus => "10000 Mbps", - Speed::Super => "5000 Mbps", - Speed::High => " 480 Mbps", - Speed::Full => " 12 Mbps", - Speed::Low => " 1.5 Mbps", - _ => "(unknown)", - } - } -}