From b3766beb742a4a8ee1127af19296f53f1ec21b05 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Fri, 10 Apr 2020 20:29:58 -0500 Subject: [PATCH] initial --- .gitignore | 1 + Cargo.lock | 625 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 19 ++ src/asciicast.rs | 65 +++++ src/main.rs | 26 ++ src/pty.rs | 200 +++++++++++++++ src/writer.rs | 46 ++++ 7 files changed, 982 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/asciicast.rs create mode 100644 src/main.rs create mode 100644 src/pty.rs create mode 100644 src/writer.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1ed4461 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,625 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "anyhow" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a60d744a80c30fcb657dfe2c1b22bcb3e814c1a1e3674f32bf5820b570fbff" + +[[package]] +name = "arc-swap" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663a8e9a99154b5fb793032533f6328da35e23aac63d5c152279aa8ba356825" + +[[package]] +name = "asciinema" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "libc", + "nix", + "parking_lot", + "serde", + "serde_derive", + "serde_json", + "signal-hook", + "termios", + "tokio", + "tokio-util", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bytes" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" + +[[package]] +name = "cc" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "fnv" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c329ae8753502fb44ae4fc2b622fa2a94652c41e795143765ba0927f92ab780" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" + +[[package]] +name = "futures-executor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f674f3e1bcb15b37284a90cedf55afdba482ab061c407a9c0ebbd0f3109741ba" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" + +[[package]] +name = "futures-macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6" + +[[package]] +name = "futures-task" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" + +[[package]] +name = "futures-util" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "hermit-abi" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e" +dependencies = [ + "libc", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "itoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0" + +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "mio" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" +dependencies = [ + "cfg-if", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.1", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e374eff525ce1c5b7687c4cef63943e7686524a387933ad27ca7ec43779cb3" +dependencies = [ + "log", + "mio", + "miow 0.3.3", + "winapi 0.3.8", +] + +[[package]] +name = "mio-uds" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396aa0f2003d7df8395cb93e09871561ccc3e785f0acb369170e8cc74ddf9226" +dependencies = [ + "socket2", + "winapi 0.3.8", +] + +[[package]] +name = "net2" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" +dependencies = [ + "cfg-if", + "libc", + "winapi 0.3.8", +] + +[[package]] +name = "nix" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "void", +] + +[[package]] +name = "num_cpus" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e136c1904604defe99ce5fd71a28d473fa60a12255d511aa78a9ddf11237aeb" +dependencies = [ + "cfg-if", + "cloudabi", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.8", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" + +[[package]] +name = "pin-utils" +version = "0.1.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" + +[[package]] +name = "proc-macro-hack" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63" + +[[package]] +name = "proc-macro-nested" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" + +[[package]] +name = "proc-macro2" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "ryu" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" + +[[package]] +name = "serde_derive" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b9f3a1686a29f53cfd91ee5e3db3c12313ec02d33765f02c1a9645a1811e2c" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" +dependencies = [ + "arc-swap", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05720e22615919e4734f6a99ceae50d00226c3c5aca406e102ebc33298214e0a" + +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.8", +] + +[[package]] +name = "syn" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termios" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0fcee7b24a25675de40d5bb4de6e41b0df07bc9856295e7e2b3a3600c400c2" +dependencies = [ + "libc", +] + +[[package]] +name = "tokio" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39fb9142eb6e9cc37f4f29144e62618440b149a138eee01a7bbe9b9226aaf17c" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-named-pipes", + "mio-uds", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "slab", + "tokio-macros", + "winapi 0.3.8", +] + +[[package]] +name = "tokio-macros" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7e473d5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "asciinema" +version = "0.1.0" +authors = ["Michael Zhang "] +edition = "2018" + +[dependencies] +anyhow = "1.0.28" +futures = "0.3.4" +libc = "0.2.68" +nix = "0.17.0" +parking_lot = "0.10.1" +serde = "1.0.106" +serde_derive = "1.0.106" +serde_json = "1.0.51" +signal-hook = "0.1.13" +termios = "0.3.2" +tokio = { version = "0.2.16", features = ["fs", "io-std", "macros", "process", "rt-core", "rt-threaded", "rt-util", "sync", "time"] } +tokio-util = { version = "0.3.1", features = ["codec"] } diff --git a/src/asciicast.rs b/src/asciicast.rs new file mode 100644 index 0000000..f0e597a --- /dev/null +++ b/src/asciicast.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; + +use serde::ser::{Serialize, SerializeSeq, Serializer}; + +#[derive(Serialize)] +pub struct Header { + version: u32, + width: u32, + height: u32, + + timestamp: Option, + duration: Option, + idle_time_limit: Option, + command: Option, + title: Option, + env: Option>, + theme: Option, +} + +#[derive(Serialize)] +pub struct Theme { + fg: String, + bg: String, + palette: String, +} + +pub struct Event<'a>(pub f64, pub EventKind<'a>); + +pub enum EventKind<'a> { + Output(&'a [u8]), + Input(&'a [u8]), +} + +impl<'a> Serialize for Event<'a> { + fn serialize(&self, s: S) -> std::result::Result { + let mut seq = s.serialize_seq(Some(3))?; + seq.serialize_element(&self.0)?; + match &self.1 { + EventKind::Output(s) => { + seq.serialize_element("o")?; + seq.serialize_element(std::str::from_utf8(s).unwrap())?; + } + EventKind::Input(s) => { + seq.serialize_element("i")?; + seq.serialize_element(std::str::from_utf8(s).unwrap())?; + } + } + seq.end() + } +} + +fn build_header(width: u32, height: u32) -> Header { + Header { + version: 2, + width, height, + + timestamp: None, + duration: None, + idle_time_limit: None, + command: None, + title: None, + env: None, + theme: None, + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a6d2ca6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,26 @@ +#![recursion_limit = "256"] + +#[macro_use] +extern crate serde_derive; + +mod asciicast; +mod pty; +mod writer; + +use std::fs::File; + +use anyhow::Result; + +use crate::writer::FileWriter; + +async fn record() -> Result<()> { + let file = File::create("output.cast")?; + pty::record(&["sh", "-c", "/usr/bin/zsh"], FileWriter::new(file))?; + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<()> { + record().await?; + Ok(()) +} diff --git a/src/pty.rs b/src/pty.rs new file mode 100644 index 0000000..39b13a0 --- /dev/null +++ b/src/pty.rs @@ -0,0 +1,200 @@ +use std::env; +use std::ffi::CString; +use std::io; +use std::os::unix::io::RawFd; +use std::time::{Instant}; + +use anyhow::Error; +use nix::{ + fcntl::{fcntl, FcntlArg, OFlag}, + ioctl_write_buf, + pty::{forkpty, Winsize}, + sys::{ + select::{select, FdSet}, + termios::{tcsetattr, SetArg, Termios}, + wait::waitpid, + }, + unistd::{execvpe, isatty, pipe, read, write, ForkResult}, +}; +use signal_hook::SigId; + +use crate::writer::Writer; + +const STDIN_FILENO: RawFd = 0; +const STDOUT_FILENO: RawFd = 1; + +pub fn record(args: &[&str], writer: impl Writer + Send) -> Result<(), Error> { + let forkpty_result = forkpty(None, None)?; + let master_fd = forkpty_result.master; + let start_time = Instant::now(); + + let set_pty_size = || -> Result<(), Error> { + ioctl_write_buf!(helper_write, libc::TIOCGWINSZ, 104, Winsize); + let winsize = Winsize { + ws_row: 24, + ws_col: 80, + ws_xpixel: 0, + ws_ypixel: 0, + }; + if isatty(STDOUT_FILENO)? {} + unsafe { helper_write(master_fd, &[winsize]) }?; + Ok(()) + }; + + let write_stdout = |data: &[u8]| -> Result<(), Error> { + write(STDOUT_FILENO, &data)?; + Ok(()) + }; + + let mut master_writer = writer.clone(); + let mut handle_master_read = move |data: &[u8]| -> Result<(), Error> { + let elapsed = (Instant::now() - start_time).as_secs_f64(); + master_writer.write_stdout(elapsed, data)?; + write_stdout(data)?; + Ok(()) + }; + + let write_master = |data: &[u8]| -> Result<(), Error> { + let mut offset = 0; + while offset < data.len() { + let len = write(master_fd, data)?; + offset += len; + } + Ok(()) + }; + + let mut stdin_writer = writer.clone(); + let mut handle_stdin_read = |data: &[u8]| -> Result<(), Error> { + write_master(data)?; + let elapsed = (Instant::now() - start_time).as_secs_f64(); + stdin_writer.write_stdin(elapsed, data)?; + Ok(()) + }; + + let mut copy = |signal_fd: RawFd| -> Result<(), Error> { + let mut fdset = FdSet::new(); + let mut buf = [0; 1024]; + + loop { + fdset.clear(); + fdset.insert(master_fd); + fdset.insert(STDIN_FILENO); + fdset.insert(signal_fd); + + select(None, &mut fdset, None, None, None)?; + + if fdset.contains(master_fd) { + let len = read(master_fd, &mut buf)?; + if len == 0 { + fdset.remove(master_fd); + } else { + handle_master_read(&buf[..len])?; + } + } else if fdset.contains(STDIN_FILENO) { + let len = read(STDIN_FILENO, &mut buf)?; + if len == 0 { + fdset.remove(STDIN_FILENO); + } else { + handle_stdin_read(&buf[..len])?; + } + } else if fdset.contains(signal_fd) { + // TODO + } + } + }; + + let child_pid = match forkpty_result.fork_result { + ForkResult::Parent { child } => child, + ForkResult::Child => { + let cstr_args: Vec<_> = args + .into_iter() + .map(|s| CString::new(*s).unwrap()) + .collect(); + let args: Vec<_> = cstr_args.iter().map(|s| s.as_ref()).collect(); + let cstr_env: Vec<_> = env::vars() + .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap()) + .collect(); + let env: Vec<_> = cstr_env.iter().map(|s| s.as_ref()).collect(); + execvpe(&args[0], &args, &env)?; + unreachable!(); + } + }; + + let (pipe_r, pipe_w) = pipe()?; + let mut flags = fcntl(pipe_w, FcntlArg::F_GETFL)?; + flags |= libc::O_NONBLOCK; + fcntl(pipe_w, FcntlArg::F_SETFL(OFlag::from_bits(flags).unwrap()))?; + + let old_handlers = set_signals( + [ + signal_hook::SIGWINCH, + signal_hook::SIGCHLD, + signal_hook::SIGHUP, + signal_hook::SIGTERM, + signal_hook::SIGQUIT, + ] + .iter() + .map(|sig| (*sig, move || eprintln!("HIT SIGNAL {:?}", *sig))), + )?; + + // set_pty_size()?; + + { + let _term = RawTerm::init(STDIN_FILENO)?; + copy(pipe_r)?; + } + unset_signals(old_handlers)?; + + waitpid(child_pid, None)?; + + Ok(()) +} + +fn set_signals(signals_list: I) -> Result, io::Error> +where + I: Iterator, + F: Fn() + Sync + Send + 'static, +{ + let mut old_handlers = Vec::new(); + for (sig, handler) in signals_list { + old_handlers.push(unsafe { signal_hook::register(sig, handler) }?); + } + Ok(old_handlers) +} + +fn unset_signals(handlers_list: Vec) -> Result<(), io::Error> { + for handler in handlers_list { + signal_hook::unregister(handler); + } + Ok(()) +} + +struct RawTerm(RawFd, Termios); + +impl RawTerm { + pub fn init(fd: RawFd) -> Result { + use nix::sys::termios::*; + let saved_mode = tcgetattr(fd)?; + let mut mode = saved_mode.clone(); + mode.input_flags &= !(InputFlags::BRKINT + | InputFlags::ICRNL + | InputFlags::INPCK + | InputFlags::ISTRIP + | InputFlags::IXON); + mode.output_flags &= !OutputFlags::OPOST; + mode.control_flags &= !(ControlFlags::CSIZE | ControlFlags::PARENB); + mode.control_flags |= ControlFlags::CS8; + mode.local_flags &= + !(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG); + mode.control_chars[libc::VMIN] = 1; + mode.control_chars[libc::VTIME] = 0; + tcsetattr(fd, SetArg::TCSAFLUSH, &mode)?; + Ok(RawTerm(fd, saved_mode)) + } +} + +impl Drop for RawTerm { + fn drop(&mut self) { + tcsetattr(self.0, SetArg::TCSAFLUSH, &self.1).unwrap(); + } +} diff --git a/src/writer.rs b/src/writer.rs new file mode 100644 index 0000000..cca4d48 --- /dev/null +++ b/src/writer.rs @@ -0,0 +1,46 @@ +use std::fs::File; +use std::io::{self, Write}; +use std::sync::Arc; + +use parking_lot::Mutex; + +// use tokio::{fs::File, io::AsyncWriteExt}; + +use crate::asciicast::{Event, EventKind}; + +pub trait Writer: Clone { + fn write_line(&mut self, line: String) -> Result<(), io::Error>; + + fn write_stdin(&mut self, time: f64, data: &[u8]) -> Result<(), anyhow::Error> { + let output = EventKind::Input(data); + let event = Event(time, output); + self.write_line(serde_json::to_string(&event)?)?; + Ok(()) + } + + fn write_stdout(&mut self, time: f64, data: &[u8]) -> Result<(), anyhow::Error> { + let output = EventKind::Output(data); + let event = Event(time, output); + self.write_line(serde_json::to_string(&event)?)?; + Ok(()) + } +} + +#[derive(Clone)] +pub struct FileWriter(Arc>); + +impl FileWriter { + pub fn new(file: File) -> Self { + FileWriter(Arc::new(Mutex::new(file))) + } +} + +impl Writer for FileWriter { + fn write_line(&mut self, line: String) -> Result<(), io::Error> { + let mut writer = self.0.lock(); + writer.write_all(line.as_bytes())?; + writer.write(b"\n")?; + writer.flush()?; + Ok(()) + } +}