From ae7fb3e6bd15d902db99d3a047821f38585bdc4c Mon Sep 17 00:00:00 2001 From: Alexander Koz Date: Sat, 30 Mar 2024 23:08:11 +0400 Subject: [PATCH] replace old version finally --- .github/workflows/dev-build.yml | 7 +- Cargo.lock | 117 ++---- Cargo.toml | 1 + support/device/Cargo.toml | 5 +- support/device/README.md | 43 +++ support/device/TODO | 3 - support/device/src/device/command.rs | 2 - support/device/src/error.rs | 2 +- support/device/src/mount/linux.rs | 18 +- support/device/src/mount/mac.rs | 2 - support/device/src/mount/methods.rs | 1 - support/device/src/mount/mod.rs | 6 + support/device/src/mount/win.rs | 13 +- support/device/src/serial/async.rs | 25 +- support/device/src/serial/blocking.rs | 63 +--- support/device/src/serial/discover.rs | 68 ++-- support/device/src/serial/methods.rs | 2 +- support/device/src/serial/mod.rs | 18 +- support/device/src/usb/discover.rs | 27 +- support/device/src/usb/mod.rs | 153 +------- support/sim-ctrl/Cargo.toml | 5 +- support/sim-ctrl/README.md | 23 +- support/tool/Cargo.toml | 50 ++- support/tool/README.md | 74 ++-- support/{tool2 => tool}/src/cli.rs | 8 - support/tool/src/cli/commands/install.rs | 183 --------- support/tool/src/cli/commands/mod.rs | 129 ------- support/tool/src/cli/commands/mount.rs | 57 --- support/tool/src/cli/commands/read.rs | 15 - support/tool/src/cli/commands/run.rs | 278 -------------- support/tool/src/cli/mod.rs | 33 -- support/tool/src/io/mod.rs | 122 ------ support/tool/src/lib.rs | 123 ------ support/tool/src/main.rs | 418 ++++++++++++++++++-- support/tool/src/model/mod.rs | 75 ---- support/tool/src/model/serial.rs | 105 ------ support/tool/src/mount.rs | 187 --------- support/{tool2 => tool}/src/report.rs | 0 support/tool/src/search/fs.rs | 35 -- support/tool/src/search/mac.rs | 239 ------------ support/tool/src/search/mod.rs | 47 --- support/tool/src/search/other.rs | 27 -- support/tool/src/search/unix.rs | 77 ---- support/tool/src/usb/io.rs | 206 ---------- support/tool/src/usb/mod.rs | 382 ------------------- support/tool2/Cargo.toml | 66 ---- support/tool2/TODO | 3 - support/tool2/src/main.rs | 461 ----------------------- 48 files changed, 665 insertions(+), 3339 deletions(-) create mode 100644 support/device/README.md delete mode 100644 support/device/TODO rename support/{tool2 => tool}/src/cli.rs (95%) delete mode 100644 support/tool/src/cli/commands/install.rs delete mode 100644 support/tool/src/cli/commands/mod.rs delete mode 100644 support/tool/src/cli/commands/mount.rs delete mode 100644 support/tool/src/cli/commands/read.rs delete mode 100644 support/tool/src/cli/commands/run.rs delete mode 100644 support/tool/src/cli/mod.rs delete mode 100644 support/tool/src/io/mod.rs delete mode 100644 support/tool/src/lib.rs delete mode 100644 support/tool/src/model/mod.rs delete mode 100644 support/tool/src/model/serial.rs delete mode 100644 support/tool/src/mount.rs rename support/{tool2 => tool}/src/report.rs (100%) delete mode 100644 support/tool/src/search/fs.rs delete mode 100644 support/tool/src/search/mac.rs delete mode 100644 support/tool/src/search/mod.rs delete mode 100644 support/tool/src/search/other.rs delete mode 100644 support/tool/src/search/unix.rs delete mode 100644 support/tool/src/usb/io.rs delete mode 100644 support/tool/src/usb/mod.rs delete mode 100644 support/tool2/Cargo.toml delete mode 100644 support/tool2/TODO delete mode 100644 support/tool2/src/main.rs diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 8498509c..57bb10df 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -82,8 +82,8 @@ jobs: - name: pdtool with - run: cargo build -p=playdate-tool2 --bin=pdtool - + continue-on-error: true + run: cargo build -p=playdate-tool --bin=pdtool - name: Upload id: upload @@ -100,7 +100,8 @@ jobs: echo 'URL: ${{ steps.upload.outputs.artifact-url }}' - name: pdtool with tracing - run: cargo build -p=playdate-tool2 --bin=pdtool --features=tracing + continue-on-error: true + run: cargo build -p=playdate-tool --bin=pdtool --features=tracing - name: Upload uses: actions/upload-artifact@v4 diff --git a/Cargo.lock b/Cargo.lock index 5ae64483..549a5441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,7 +150,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -161,7 +161,7 @@ checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -282,7 +282,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.55", + "syn 2.0.57", "which 4.4.2", ] @@ -383,9 +383,9 @@ dependencies = [ [[package]] name = "cargo" -version = "0.78.0" +version = "0.78.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40aa4f7f65beb7840194ee32d91f7d0ee0165bbf85e368bbb2c8d6389ea4fabb" +checksum = "d6305e39d08315644d79a5ae09a0745dfb3a43b5b5e318e55dbda3f12031c5dc" dependencies = [ "annotate-snippets", "anstream", @@ -678,7 +678,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -1305,7 +1305,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -1755,7 +1755,7 @@ checksum = "1dff438f14e67e7713ab9332f5fd18c8f20eb7eb249494f6c2bf170522224032" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -2583,7 +2583,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -2711,18 +2711,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "libusb1-sys" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "libz-sys" version = "1.1.16" @@ -2827,14 +2815,14 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "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" @@ -2882,7 +2870,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -3095,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", @@ -3378,14 +3366,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "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 = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -3441,7 +3429,7 @@ dependencies = [ "proc-macro2", "quote", "semver", - "syn 2.0.55", + "syn 2.0.57", "which 6.0.1", ] @@ -3615,24 +3603,7 @@ dependencies = [ [[package]] name = "playdate-tool" -version = "0.1.4" -dependencies = [ - "clap", - "env_logger", - "log", - "playdate-build", - "plist", - "regex", - "rusb", - "serde", - "serde_json", - "thiserror", - "usb-ids", -] - -[[package]] -name = "playdate-tool2" -version = "0.2.0-alpha1" +version = "0.2.0" dependencies = [ "clap", "console-subscriber", @@ -3712,7 +3683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" dependencies = [ "proc-macro2", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -3778,7 +3749,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -3939,16 +3910,6 @@ 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" @@ -4052,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", @@ -4065,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", @@ -4119,7 +4080,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -4416,9 +4377,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" dependencies = [ "proc-macro2", "quote", @@ -4508,7 +4469,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -4571,9 +4532,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -4607,7 +4568,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -4773,7 +4734,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -5014,7 +4975,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", "wasm-bindgen-shared", ] @@ -5036,7 +4997,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5330,7 +5291,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -5380,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 be28de7a..20976e8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,3 +48,4 @@ serde_json = "1.0" toml = "0.8" futures-lite = "2.3" thiserror = "1.0" +tokio = { version = "1.37", default-features = false } diff --git a/support/device/Cargo.toml b/support/device/Cargo.toml index e7da120c..139b06a0 100644 --- a/support/device/Cargo.toml +++ b/support/device/Cargo.toml @@ -13,7 +13,6 @@ repository.workspace = true [dependencies] -# TODO: remove probably object-pool = "0.5" regex.workspace = true @@ -28,7 +27,7 @@ tokio-serial = { version = "5.4", optional = true } tracing = { version = "0.1", optional = true } -# mb. read mount-points: +# mb. read mount-points more correctly: # rustix = "0.38" serde = { workspace = true, features = ["derive"] } @@ -36,8 +35,8 @@ serde_json.workspace = true hex = "0.4" [dependencies.tokio] -version = "1.36" features = ["fs", "process", "time", "io-std"] +workspace = true optional = true [dependencies.futures-lite] 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/TODO b/support/device/TODO deleted file mode 100644 index a84e822c..00000000 --- a/support/device/TODO +++ /dev/null @@ -1,3 +0,0 @@ - -README: -- add `libudev-sys` to linux deps diff --git a/support/device/src/device/command.rs b/support/device/src/device/command.rs index af4d19b4..329a9681 100644 --- a/support/device/src/device/command.rs +++ b/support/device/src/device/command.rs @@ -33,8 +33,6 @@ pub enum Command { /// Turn console echo on or off. Echo { - // #[cfg_attr(feature = "clap", arg(default_value_t = true))] - // value: bool, #[cfg_attr(feature = "clap", arg(default_value_t = Switch::On))] value: Switch, }, diff --git a/support/device/src/error.rs b/support/device/src/error.rs index 107df956..eec4aeab 100644 --- a/support/device/src/error.rs +++ b/support/device/src/error.rs @@ -92,7 +92,7 @@ pub enum Error { source: windows::core::Error, }, - #[error("Chain of errors ending with: {source}")] + #[error("Chain of errors: {source}\n\t{others:#?}")] #[diagnostic()] Chain { #[backtrace] diff --git a/support/device/src/mount/linux.rs b/support/device/src/mount/linux.rs index 3ecb2aa2..8064f0ab 100644 --- a/support/device/src/mount/linux.rs +++ b/support/device/src/mount/linux.rs @@ -1,5 +1,3 @@ -// #![cfg(target_os = "linux")] - use std::borrow::Cow; use std::collections::HashMap; use std::ffi::OsStr; @@ -71,19 +69,19 @@ mod unmount { unmount(self).status() .map_err(Error::from) .and_then(|res| res.exit_ok().map_err(Error::from)) - .map_err(|err2| Error::chain(err, [err2])) + .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(err, [err2])) + .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(err, [err2])) + .map_err(|err2| Error::chain(err2, [err])) }) .inspect(|_| trace!("unmounted {self}")); @@ -93,7 +91,7 @@ mod unmount { .and_then(|res| res.exit_ok().map_err(Error::from)) .map_err(move |err2| { if let Some(err) = res.err() { - Error::chain(err, [err2]) + Error::chain(err2, [err]) } else { err2 } @@ -114,7 +112,7 @@ mod unmount { .and_then(|res| ready(res.exit_ok().map_err(Error::from))) .or_else(|err| { Command::from(unmount(self)).status() - .map_err(|err2| Error::chain(err, [err2])) + .map_err(|err2| Error::chain(err2, [err])) .and_then(|res| { ready(res.exit_ok().map_err(Error::from)) }) @@ -122,7 +120,7 @@ mod unmount { .or_else(|err| { Command::from(udisksctl_unmount(self)).status() .map_err(|err2| { - Error::chain(err, [err2]) + Error::chain(err2, [err]) }) .and_then(|res| { ready( @@ -134,7 +132,7 @@ mod unmount { .or_else(|err| { Command::from(udisks_unmount(self)).status() .map_err(|err2| { - Error::chain(err, [err2]) + Error::chain(err2, [err]) }) .and_then(|res| { ready( @@ -156,7 +154,7 @@ mod unmount { }) .map_err(|err2| { if let Some(err) = res.err() { - Error::chain(err, [err2]) + Error::chain(err2, [err]) } else { err2 } diff --git a/support/device/src/mount/mac.rs b/support/device/src/mount/mac.rs index eac44727..63be857a 100644 --- a/support/device/src/mount/mac.rs +++ b/support/device/src/mount/mac.rs @@ -1,5 +1,3 @@ -// #![cfg(target_os = "macos")] - use std::borrow::Cow; use std::collections::HashMap; use std::path::Path; diff --git a/support/device/src/mount/methods.rs b/support/device/src/mount/methods.rs index 8dc47c92..1c4eef3e 100644 --- a/support/device/src/mount/methods.rs +++ b/support/device/src/mount/methods.rs @@ -237,7 +237,6 @@ fn mount_dev(mut dev: Device) -> Result { trace!("create sending fut"); async move { - use interface::r#async::Out; dev.open()?; dev.interface()? .send_cmd(crate::device::command::Command::Datadisk) diff --git a/support/device/src/mount/mod.rs b/support/device/src/mount/mod.rs index dfc398ca..59d02988 100644 --- a/support/device/src/mount/mod.rs +++ b/support/device/src/mount/mod.rs @@ -21,6 +21,12 @@ 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>; diff --git a/support/device/src/mount/win.rs b/support/device/src/mount/win.rs index c06abac6..cd02dbe5 100644 --- a/support/device/src/mount/win.rs +++ b/support/device/src/mount/win.rs @@ -1,5 +1,3 @@ -#![cfg(target_os = "windows")] - extern crate windows; use std::borrow::Cow; use std::collections::HashMap; @@ -55,7 +53,7 @@ mod unmount { .and_then(|res| { res.exit_ok().map_err(Error::from) }) - .map_err(|err2| Error::chain(err, [err2])) + .map_err(|err2| Error::chain(err2, [err])) } else { Err(err) } @@ -64,7 +62,7 @@ mod unmount { eject_pw(self.letter).status() .map_err(Error::from) .and_then(|res| res.exit_ok().map_err(Error::from)) - .map_err(|err2| Error::chain(err, [err2])) + .map_err(|err2| Error::chain(err2, [err])) }) } } @@ -79,7 +77,7 @@ mod unmount { 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(err, [err2])) + .map_err(|err2| Error::chain(err2, [err])) .and_then(|res| ready(res.exit_ok().map_err(Error::from))) .left_future() } else { @@ -88,7 +86,7 @@ mod unmount { }) .or_else(|err| { Command::from(eject_pw(self.letter)).status() - .map_err(|err2| Error::chain(err, [err2])) + .map_err(|err2| Error::chain(err2, [err])) .and_then(|res| ready(res.exit_ok().map_err(Error::from))) }) .await @@ -104,6 +102,7 @@ mod unmount { } 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); @@ -332,7 +331,7 @@ mod winapi { debug!("{err}, trying fallback method..."); let (string, s) = winapi::pcstr_short(letter); unsafe { DeleteVolumeMountPointA(s) }.map(|_| drop(string)) - .map_err(|err2| Error::chain(err, [err2])) + .map_err(|err2| Error::chain(err2, [err])) }) } diff --git a/support/device/src/serial/async.rs b/support/device/src/serial/async.rs index 4a47f42c..acea0c51 100644 --- a/support/device/src/serial/async.rs +++ b/support/device/src/serial/async.rs @@ -1,12 +1,8 @@ #![cfg(feature = "tokio-serial")] +#![cfg(feature = "tokio")] use std::ops::DerefMut; -// use futures::TryFutureExt; -// use futures::AsyncWriteExt; -// use futures_lite::AsyncWriteExt; -// #[allow(unused_imports)] -// use tokio::io::AsyncWrite; use tokio::io::AsyncWriteExt; use crate::error::Error; @@ -30,21 +26,4 @@ impl crate::interface::r#async::Out for Interface { } -// impl crate::interface::blocking::Out for T where T: serialport::SerialPort {} -// impl crate::interface::r#async::In for T where T: DerefMut {} - -// impl crate::interface::r#async::Out for T where T: DerefMut { -// impl crate::interface::r#async::Out for T where T: DerefMut { -// async fn send_cmd(&self, cmd: crate::device::command::Command) -> Result { -// let port = self.deref_mut(); -// let cmd = cmd.with_break(); -// port.write_all(cmd.as_bytes()).await?; -// port.flush().await?; -// Ok(cmd.as_bytes().len()) -// } -// } - - -impl crate::interface::r#async::In for Interface { - // type Error = crate::error::Error; -} +impl crate::interface::r#async::In for Interface {} diff --git a/support/device/src/serial/blocking.rs b/support/device/src/serial/blocking.rs index 0e707953..08bcfac5 100644 --- a/support/device/src/serial/blocking.rs +++ b/support/device/src/serial/blocking.rs @@ -20,65 +20,4 @@ impl crate::interface::blocking::Out for Interface { } } -impl crate::interface::blocking::In for Interface { - // type Error = crate::error::Error; -} - - -// impl crate::interface::blocking::OutInterface for Interface { -// type Error = Error; - -// fn send(&self, buf: &[u8]) -> Result { -// if let Some(ref port) = self.port { -// let mut port = port.try_borrow_mut()?; -// Ok(port.write(buf).and_then(|len| port.flush().and(Ok(len)))?) -// } else { -// Err(Error::not_ready()) -// } -// } - -// fn send_cmd(&self, cmd: crate::device::command::Command) -> Result { -// self.send(cmd.with_break().as_bytes()) -// } -// } - - -// impl Write for Interface { -// fn write(&mut self, buf: &[u8]) -> std::io::Result { -// if let Some(ref port) = self.port { -// port.try_borrow_mut()?.write(buf) -// } else { -// Err(std::io::Error::new(std::io::ErrorKind::NotConnected, Error::not_ready())) -// } -// } - -// fn flush(&mut self) -> std::io::Result<()> { -// if let Some(ref port) = self.port { -// port.try_borrow_mut()?.flush() -// } else { -// Err(std::io::Error::new(std::io::ErrorKind::NotConnected, Error::not_ready())) -// } -// } -// } - - -// impl futures::AsyncWrite for Interface { -// fn poll_write(self: std::pin::Pin<&mut Self>, -// cx: &mut std::task::Context<'_>, -// buf: &[u8]) -// -> std::task::Poll> { -// todo!() -// } - -// fn poll_flush(self: std::pin::Pin<&mut Self>, -// cx: &mut std::task::Context<'_>) -// -> std::task::Poll> { -// todo!() -// } - -// fn poll_close(self: std::pin::Pin<&mut Self>, -// cx: &mut std::task::Context<'_>) -// -> std::task::Poll> { -// todo!() -// } -// } +impl crate::interface::blocking::In for Interface {} diff --git a/support/device/src/serial/discover.rs b/support/device/src/serial/discover.rs index 0cbeae26..1c67f369 100644 --- a/support/device/src/serial/discover.rs +++ b/support/device/src/serial/discover.rs @@ -35,6 +35,7 @@ pub fn port(sn: &SN) -> Result _ => false, } }); + // TODO: error: serial not found for sn port.ok_or_else(|| Error::not_found()) } @@ -44,29 +45,10 @@ pub fn port(sn: &SN) -> Result #[cfg_attr(feature = "tracing", tracing::instrument())] pub fn ports_with(sn: Option) -> Result, Error> where SN: PartialEq + Debug { - let ports = ports()?.filter(move |port| { - match port.port_type { - SerialPortType::UsbPort(ref info) => { - if let Some(sn) = sn.as_ref() { - trace!("found port: {}, dev sn: {:?}", port.port_name, info.serial_number); - info.serial_number - .as_ref() - .filter(|s| { - let res = sn.eq(s); - trace!("sn is ≈ {res}"); - res - }) - .is_some() - } else { - true - } - }, - _ => false, - } - }); - Ok(ports) + 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`. /// @@ -89,32 +71,34 @@ pub fn ports_with_or_single(sn: Option) -> Result { - if let Some(sn) = sn.as_ref() { - trace!("found port: {}, dev sn: {:?}", port.port_name, info.serial_number); - info.serial_number - .as_ref() - .filter(|s| { - let res = sn.eq(s); - trace!("sn is ≈ {res}"); - res - }) - .is_some() - } else { - true - } - }, - _ => false, - } - }); + 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"))] diff --git a/support/device/src/serial/methods.rs b/support/device/src/serial/methods.rs index a6e5deec..637fd84b 100644 --- a/support/device/src/serial/methods.rs +++ b/support/device/src/serial/methods.rs @@ -16,7 +16,6 @@ pub async fn dev_with_port>(port: S) -> Result { let name = port.as_ref(); let port = super::open(name)?; - trace!("opened port {name}"); let dev = port.as_ref() .name() @@ -32,6 +31,7 @@ pub async fn dev_with_port>(port: S) -> Result { Some(dev) }); + // TODO: error: device not found for serial port dev.ok_or_else(|| Error::not_found()) } diff --git a/support/device/src/serial/mod.rs b/support/device/src/serial/mod.rs index 3c0c5dfb..1a409d78 100644 --- a/support/device/src/serial/mod.rs +++ b/support/device/src/serial/mod.rs @@ -5,8 +5,8 @@ use crate::error::Error; pub mod discover; -pub mod blocking; -pub mod r#async; +mod blocking; +mod r#async; mod methods; pub use methods::*; @@ -95,17 +95,27 @@ impl Interface { #[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"))] { - Ok(builder.open()?) + port = builder.open()?; } #[cfg(feature = "tokio-serial")] { use tokio_serial::SerialPortBuilderExt; - Ok(builder.open_native_async().map(Box::new)?) + 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 { diff --git a/support/device/src/usb/discover.rs b/support/device/src/usb/discover.rs index e767089a..1b2f949f 100644 --- a/support/device/src/usb/discover.rs +++ b/support/device/src/usb/discover.rs @@ -11,29 +11,31 @@ 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, Error> { +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, Error> { +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, Error> { +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 { +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()) } @@ -41,7 +43,7 @@ pub fn device(sn: &Sn) -> Result { /// 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, Error> { +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() @@ -54,7 +56,7 @@ pub fn devices_with(sn: Option) -> Result, Erro /// 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, Error> { +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() @@ -67,7 +69,7 @@ pub fn devices_data_with(sn: Option) -> Result, #[cfg(feature = "futures")] #[cfg_attr(feature = "tracing", tracing::instrument)] -pub async fn devices_data_for(query: query::Query) -> Result, Error> { +pub async fn devices_data_for(query: query::Query) -> Result> { use query::Value as Query; use serial::dev_with_port; @@ -109,7 +111,7 @@ pub async fn devices_data_for(query: query::Query) -> Result, Error> Ok(None) => dev_with_port(port_pref).await, Err(err) => { dev_with_port(port_pref).await - .map_err(|err2| Error::chain(err, [err2])) + .map_err(|err2| Error::chain(err2, [err])) }, } } @@ -131,12 +133,9 @@ pub async fn devices_data_for(query: query::Query) -> Result, Error> #[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, Error> - where Fut: std::future::Future, /* + Unpin */ - F: FnMut(interface::Interface) -> Fut -{ +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; diff --git a/support/device/src/usb/mod.rs b/support/device/src/usb/mod.rs index 8bacdc89..ece50d64 100644 --- a/support/device/src/usb/mod.rs +++ b/support/device/src/usb/mod.rs @@ -16,7 +16,6 @@ use object_pool::Reusable; use crate::device::command::Command; use crate::device::Device; use crate::error::Error; -use crate::interface::blocking::Out; use self::mode::DeviceMode; use self::mode::Mode; @@ -154,7 +153,7 @@ impl Device { 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(err, [err2])) + self.try_serial().map_err(|err2| Error::chain(err2, [err])) } else { self.interface() } @@ -247,158 +246,8 @@ impl Device { dev.reset().map_err(|err| error!("{err}")).ok(); } } - - - pub fn debug_inspect(&mut self) { - inspect_device(self.info()); - - // try interfaces: - - self.open().unwrap(); - // let device = self. - - // - } } - -/// Print debug information about device. -pub fn inspect_device(info: &DeviceInfo) { - println!( - "Device {:03}.{:03} ({:04x}:{:04x}) {} {}", - info.bus_number(), - info.device_address(), - info.vendor_id(), - info.product_id(), - info.manufacturer_string().unwrap_or(""), - info.product_string().unwrap_or("") - ); - - println!(" {info:#?}"); - - - let bulk = info.data_interface_number(); - let mut has_data_interface = bulk.is_some(); - let mut bulk_interface_number = None; - println!("bulk interface: {:#?}", bulk); - println!("---"); - - - let describe_class = |class: u8, subclass: u8, protocol: u8, indent: &'static str| { - use usb_ids::FromId; - - let class = usb_ids::Class::from_id(class); - - let subs = class.unwrap() - .sub_classes() - .filter(|sub| sub.id() == subclass) - .collect::>(); - - - for sub in subs { - println!("{indent}sub: ({:#02x}) {}", sub.id(), sub.name()); - let protocols = sub.protocols().filter(|p| p.id() == protocol).collect::>(); - if protocols.is_empty() { - println!("{indent}{indent}unknown protocol: {protocol:#02x}"); - } - for p in protocols { - println!("{indent}{indent}protocol: ({:#02x}) {}", p.id(), p.name()); - } - } - }; - - { - use usb_ids::FromId; - - let interfaces = info.interfaces().collect::>(); - println!("interfaces: ({})", interfaces.len()); - for i in interfaces { - let class = usb_ids::Class::from_id(i.class()); - - let n = i.interface_number(); - let name = i.interface_string().unwrap_or("_"); - - println!("{n}: {name}"); - println!("class: ({:#02x}) {:?}", i.class(), class.map(|c| c.name())); - describe_class(i.class(), i.subclass(), i.protocol(), " "); - } - } - println!("---"); - - - let dev = match info.open() { - Ok(dev) => dev, - Err(e) => { - println!("Failed to open device: {}", e); - return; - }, - }; - - let active_configuration = dev.active_configuration(); - match &active_configuration { - Ok(config) => println!("Active configuration is {}", config.configuration_value()), - Err(e) => println!("Unknown active configuration: {e}"), - } - - - for config in dev.configurations() { - let active = if let Ok(ref cfg) = active_configuration { - if config.configuration_value() == cfg.configuration_value() { - "[ACTIVE] " - } else { - "" - } - } else { - "" - }; - - println!(" {active}{config:#?}"); - - // if !has_data_interface - { - println!(" |"); - println!(" |"); - println!(" \\---"); - for i in config.interfaces() { - let bulk = i.alt_settings().find(|i| i.class() == 0xA | 2); - if let Some(bulk) = bulk { - println!("I JUST FOUND BULK INTERFACE!"); - println!("{bulk:#?}"); - has_data_interface = true; - bulk_interface_number = Some(bulk.interface_number()); - } else { - for i in i.alt_settings() { - describe_class(i.class(), i.subclass(), i.protocol(), " "); - println!(" endpoints: ({})", i.num_endpoints()); - for endpoint in i.endpoints() { - println!(" {endpoint:#?}"); - } - } - } - } - } - } - - println!("---"); - - if has_data_interface && bulk_interface_number.is_some() { - let id = bulk_interface_number.unwrap(); - println!("trying to open interface: {id}"); - let i = Interface::from(dev.claim_interface(id).unwrap()); - { - use crate::device::command::SystemPath; - use crate::device::command::Switch; - - i.send_cmd(Command::Echo { value: Switch::On }).unwrap(); - i.send_cmd(Command::RunSystem { path: SystemPath::Settings }) - .unwrap(); - } - } - - println!("----------------\n"); -} - - impl crate::interface::blocking::Out for Interface { #[cfg_attr(feature = "tracing", tracing::instrument)] fn send_cmd(&self, cmd: Command) -> Result { diff --git a/support/sim-ctrl/Cargo.toml b/support/sim-ctrl/Cargo.toml index 45f38337..273eb49a 100644 --- a/support/sim-ctrl/Cargo.toml +++ b/support/sim-ctrl/Cargo.toml @@ -2,7 +2,7 @@ name = "playdate-simulator-utils" version = "0.1.0" readme = "README.md" -description = "Cross-platform utils to deal with Simulator in the Playdate SDK." +description = "Cross-platform utils to deal with Playdate Simulator." keywords = ["playdate", "sdk", "utils"] categories = ["development-tools"] edition.workspace = true @@ -22,6 +22,7 @@ workspace = true default-features = false [dependencies.tokio] -version = "1.36" features = ["process"] +default-features = false +workspace = true optional = true diff --git a/support/sim-ctrl/README.md b/support/sim-ctrl/README.md index 28eb0014..2843f61f 100644 --- a/support/sim-ctrl/README.md +++ b/support/sim-ctrl/README.md @@ -1,9 +1,9 @@ # Playdate Simulator Utils -Cross-platform utils to run Simulator in the Playdate SDK. +Cross-platform utils to do things with Playdate Simulator. -Example: +Usage: ```rust let pdx = PathBuf::from("path/to/my-game.pdx"); @@ -15,10 +15,27 @@ 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!("{}", std::str::from_utf8(&stdout).unwrap()); +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. 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/tool2/src/cli.rs b/support/tool/src/cli.rs similarity index 95% rename from support/tool2/src/cli.rs rename to support/tool/src/cli.rs index 1d8365ef..a11437ec 100644 --- a/support/tool2/src/cli.rs +++ b/support/tool/src/cli.rs @@ -100,14 +100,6 @@ pub struct Dbg { pub enum DbgCmd { /// Inspect device(s) state. Inspect, - /// Probe powershell - Probe, - /// Retrieve sn of dev that behind the mounted volume. - VolSn1 { vol: char }, - /// Retrieve sn of dev that behind the mounted volume. - VolSn2 { vol: char }, - /// Eject device by mounted volume letter. - Eject { vol: char }, } 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 13c5cbcd..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 = "mount")] -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/model/serial.rs b/support/tool/src/model/serial.rs deleted file mode 100644 index ce161220..00000000 --- a/support/tool/src/model/serial.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::borrow::Cow; -use std::path::Path; -use std::str::FromStr; -use regex::Regex; - - -/// Represents a device serial number. -/// e.g.: PDU1-Y000235 -#[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 { - pub const REGEX_NAME: &str = r"^.*(PDU\d+[_-][a-zA-Z0-9]+).*$"; - let re = Regex::new(REGEX_NAME).expect("invalid regex"); - let captures = re.captures(s.as_ref())?; - let serial = Self::unify(captures.get(1)?.as_str()); - let serial = if serial.contains("_") { - serial.replace("_", "-") - } else { - serial.to_string() - }; - - Some(Self(serial.to_owned())) - } - - - fn unify<'s, S: Into>>(s: S) -> Cow<'s, str> { - let s = s.into(); - if s.contains("_") { - s.replace("_", "-").into() - } else { - s - } - } -} - -impl FromStr for SerialNumber { - type Err = DeviceSerialFormatError; - fn from_str(s: &str) -> Result { - Self::contained_in(s).ok_or(DeviceSerialFormatError(s.to_string())) - } -} - -impl TryFrom for SerialNumber { - type Error = ::Err; - fn try_from(value: String) -> Result { Self::from_str(value.as_str()) } -} - -impl TryFrom<&str> for SerialNumber { - type Error = ::Err; - fn try_from(value: &str) -> Result { Self::from_str(value) } -} - -impl TryFrom<&Path> for SerialNumber { - type Error = ::Err; - fn try_from(value: &Path) -> Result { Self::from_str(value.to_string_lossy().as_ref()) } -} - - -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)) - } -} - -impl std::fmt::Debug for SerialNumber { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Serial").field(&self.0).finish() - } -} - -impl std::fmt::Display for SerialNumber { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } -} - - -#[derive(Debug)] -pub struct DeviceSerialFormatError(String); -impl std::error::Error for DeviceSerialFormatError {} - -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 - ) - } -} - -impl From for DeviceSerialFormatError { - fn from(value: String) -> Self { Self(value) } -} - -impl From<&str> for DeviceSerialFormatError { - fn from(value: &str) -> Self { Self(value.to_owned()) } -} 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/tool2/src/report.rs b/support/tool/src/report.rs similarity index 100% rename from support/tool2/src/report.rs rename to support/tool/src/report.rs 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)", - } - } -} diff --git a/support/tool2/Cargo.toml b/support/tool2/Cargo.toml deleted file mode 100644 index 8a0e13a1..00000000 --- a/support/tool2/Cargo.toml +++ /dev/null @@ -1,66 +0,0 @@ -[package] -name = "playdate-tool2" -version = "0.2.0-alpha1" -readme = "README.md" -description = "Tool for interaction with Playdate device and sim." -keywords = ["playdate", "usb", "utility"] -categories = ["development-tools", "hardware-support"] -edition.workspace = true -license.workspace = true -authors.workspace = true -homepage.workspace = true -repository.workspace = true - - -[[bin]] -path = "src/main.rs" -name = "pdtool" -# required-features = ["tracing"] - - -[dependencies] -# RT, async: -tokio = { version = "1.36", features = ["full", "rt-multi-thread"] } -futures = { version = "0.3" } -futures-lite.workspace = true - -serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true - -# CLI: -log.workspace = true -env_logger.workspace = true -thiserror.workspace = true -miette = { version = "7.2", features = ["fancy"] } - -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 - -# PD: -[dependencies.device] -features = ["async", "tokio", "clap", "tokio-serial"] -workspace = true - -[dependencies.simulator] -features = ["tokio"] -workspace = true - - -[features] -default = ["tracing"] -tracing = [ - "dep:tracing", - "tracing-subscriber", - "device/tracing", - "simulator/tracing", -] - -# use with tokio-console, https://tokio.rs/tokio/topics/tracing-next-steps -tokio-tracing = ["tracing", "tokio/tracing", "console-subscriber", "tracing"] diff --git a/support/tool2/TODO b/support/tool2/TODO deleted file mode 100644 index a84e822c..00000000 --- a/support/tool2/TODO +++ /dev/null @@ -1,3 +0,0 @@ - -README: -- add `libudev-sys` to linux deps diff --git a/support/tool2/src/main.rs b/support/tool2/src/main.rs deleted file mode 100644 index b39e8ddd..00000000 --- a/support/tool2/src/main.rs +++ /dev/null @@ -1,461 +0,0 @@ -#![feature(exitcode_exit_method)] -#![feature(exit_status_error)] - -#[cfg(feature = "tracing")] -#[macro_use] -extern crate tracing; - -#[cfg(not(feature = "tracing"))] -#[macro_use] -extern crate log; - -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; - - -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"))] - { - // std::env::set_var("RUST_LOG", "trace,nusb=warn,mio_serial=info,mio=info"); - 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(); - - - 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?, - Cmd::Probe => probe().await?, - Cmd::VolSn1 { vol } => vol_sn_1(vol).await?, - Cmd::VolSn2 { vol } => vol_sn_2(vol).await?, - Cmd::Eject { vol } => eject(vol).await?, - } - - Ok(()) - } - - pub async fn vol_sn_1(letter: char) -> Result<(), Error> { - use tokio::process::Command; - - let arg = - format!("Get-Partition -DriveLetter {letter} | Get-Disk | select-object -ExpandProperty SerialNumber"); - let mut cmd = Command::new("powershell"); - cmd.arg(arg); - let output = cmd.output().await?; - println!("out: {:?}", std::str::from_utf8(&output.stdout)); - println!("err: {:?}", std::str::from_utf8(&output.stderr)); - println!("---\n> {output:#?}"); - Ok(()) - } - - // ExitStatus always 0 :( - // Err if stderr is not empty OR stdout is empty - pub async fn vol_sn_2(letter: char) -> Result<(), Error> { - use tokio::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 -Property DiskIndex,SerialNumber"); - let mut cmd = Command::new("powershell"); - cmd.arg(arg); - let output = cmd.output().await?; - println!("out: {:?}", std::str::from_utf8(&output.stdout)); - println!("err: {:?}", std::str::from_utf8(&output.stderr)); - println!("---\n> {output:#?}"); - Ok(()) - } - - pub async fn eject(letter: char) -> Result<(), Error> { - use tokio::process::Command; - - let arg = format!("(New-Object -comObject Shell.Application).NameSpace(17).ParseName('{letter}:').InvokeVerb('Eject') | Wait-Process"); - let mut cmd = Command::new("powershell"); - cmd.arg(arg); - cmd.status().await?.exit_ok()?; - Ok(()) - } - - pub async fn probe() -> Result<(), Error> { - use tokio::process::Command; - - Command::new("powershell").status().await?.exit_ok()?; - 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(|(mut dev, path)| { - dev.debug_inspect(); - 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(()) -} - - -/// `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()))); - - match format { - cli::Format::Human => { - for (mut dev, vol) in devices { - if !dev.is_ready() { - dev.open().ok(); - } - - 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(); - } - - 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(); - - if i != len - 1 { - print!(", "); - } - } - println!("]"); - }, - } - - Ok(()) -}