abscissa_core-0.8.2/.cargo_vcs_info.json0000644000000001420000000000100136400ustar { "git": { "sha1": "a35938142f55ae922e2e4a131ace5ea4b34de3e7" }, "path_in_vcs": "core" }abscissa_core-0.8.2/Cargo.lock0000644000000523070000000000100116250ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "abscissa_core" version = "0.8.2" dependencies = [ "abscissa_derive", "arc-swap", "backtrace", "canonical-path", "clap", "color-eyre", "fs-err", "once_cell", "regex", "secrecy", "semver", "serde", "termcolor", "toml", "tracing", "tracing-log", "tracing-subscriber", "wait-timeout", ] [[package]] name = "abscissa_derive" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d914621d2ef4da433fe01907e323ee3f2807738d392d5a34c287b381f87fe2" dependencies = [ "ident_case", "proc-macro2", "quote", "syn 1.0.109", "synstructure", ] [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", "windows-sys", ] [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "canonical-path" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e9e01327e6c86e92ec72b1c798d4a94810f147209bbe3ffab6a86954937a6f" [[package]] name = "cc" version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck", "proc-macro2", "quote", "syn 2.0.96", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "color-eyre" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" dependencies = [ "backtrace", "eyre", "indenter", "once_cell", "owo-colors", ] [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "eyre" version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", ] [[package]] name = "fs-err" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" dependencies = [ "autocfg", ] [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indenter" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "log" version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ "regex-automata 0.1.10", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "proc-macro2" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.9", "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "secrecy" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" dependencies = [ "serde", "zeroize", ] [[package]] name = "semver" version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" dependencies = [ "serde", ] [[package]] name = "serde" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", "syn 2.0.96", ] [[package]] name = "serde_spanned" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", "unicode-xid", ] [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "toml" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", "syn 2.0.96", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", ] [[package]] name = "unicode-ident" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[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-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys", ] [[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 = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310" dependencies = [ "memchr", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" abscissa_core-0.8.2/Cargo.toml0000644000000056730000000000100116540ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.74" name = "abscissa_core" version = "0.8.2" authors = ["Tony Arcieri "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = """ Application microframework with support for command-line option parsing, configuration, error handling, logging, and terminal interactions. This crate contains the framework's core functionality. """ homepage = "https://github.com/iqlusioninc/abscissa/" readme = "README.md" keywords = [ "abscissa", "cli", "application", "framework", "service", ] categories = [ "command-line-interface", "config", "rust-patterns", ] license = "Apache-2.0" repository = "https://github.com/iqlusioninc/abscissa/tree/main/core/" [lib] name = "abscissa_core" path = "src/lib.rs" [[test]] name = "component" path = "tests/component.rs" [dependencies.abscissa_derive] version = "0.8" [dependencies.arc-swap] version = "1" optional = true [dependencies.backtrace] version = "0.3" [dependencies.canonical-path] version = "2" [dependencies.clap] version = "4" features = ["derive"] optional = true [dependencies.color-eyre] version = "0.6" optional = true default-features = false [dependencies.fs-err] version = "2" [dependencies.once_cell] version = "1.17" [dependencies.regex] version = "1" optional = true [dependencies.secrecy] version = "0.10" features = ["serde"] optional = true [dependencies.semver] version = "1" optional = true [dependencies.serde] version = "1" features = ["serde_derive"] optional = true [dependencies.termcolor] version = "1" optional = true [dependencies.toml] version = "0.8" optional = true [dependencies.tracing] version = "0.1" optional = true [dependencies.tracing-log] version = "0.2" optional = true [dependencies.tracing-subscriber] version = "0.3" features = [ "fmt", "env-filter", "ansi", "smallvec", "tracing-log", ] optional = true default-features = false [dependencies.wait-timeout] version = "0.2" optional = true [features] application = [ "arc-swap", "config", "trace", "options", "semver/serde", "terminal", ] config = [ "secrets", "serde", "terminal", "toml", ] default = [ "application", "secrets", "testing", ] options = ["clap"] secrets = ["secrecy"] terminal = [ "color-eyre", "termcolor", ] testing = [ "regex", "wait-timeout", ] trace = [ "tracing", "tracing-log", "tracing-subscriber", ] abscissa_core-0.8.2/Cargo.toml.orig000064400000000000000000000041321046102023000153220ustar 00000000000000[package] name = "abscissa_core" description = """ Application microframework with support for command-line option parsing, configuration, error handling, logging, and terminal interactions. This crate contains the framework's core functionality. """ version = "0.8.2" license = "Apache-2.0" authors = ["Tony Arcieri "] homepage = "https://github.com/iqlusioninc/abscissa/" repository = "https://github.com/iqlusioninc/abscissa/tree/main/core/" readme = "../README.md" categories = ["command-line-interface", "config", "rust-patterns"] keywords = ["abscissa", "cli", "application", "framework", "service"] edition = "2021" rust-version = "1.74" [dependencies] abscissa_derive = { version = "0.8", path = "../derive" } backtrace = "0.3" canonical-path = "2" fs-err = "2" once_cell = "1.17" # optional dependencies arc-swap = { version = "1", optional = true } color-eyre = { version = "0.6", optional = true, default-features = false } clap = { version = "4", optional = true, features = ["derive"] } regex = { version = "1", optional = true } secrecy = { version = "0.10", optional = true, features = ["serde"] } semver = { version = "1", optional = true } serde = { version = "1", optional = true, features = ["serde_derive"] } termcolor = { version = "1", optional = true } toml = { version = "0.8", optional = true } tracing = { version = "0.1", optional = true } tracing-log = { version = "0.2", optional = true } wait-timeout = { version = "0.2", optional = true } [dependencies.tracing-subscriber] version = "0.3" optional = true default-features = false features = ["fmt", "env-filter", "ansi", "smallvec", "tracing-log"] [features] default = [ "application", "secrets", "testing", ] application = [ "arc-swap", "config", "trace", "options", "semver/serde", "terminal" ] config = [ "secrets", "serde", "terminal", "toml" ] options = ["clap"] secrets = ["secrecy"] terminal = ["color-eyre", "termcolor"] testing = ["regex", "wait-timeout"] trace = [ "tracing", "tracing-log", "tracing-subscriber" ] abscissa_core-0.8.2/README.md000064400000000000000000000216471046102023000137240ustar 00000000000000# ![Abscissa][logo] [![Crate][crate-image]][crate-link] [![Docs][docs-image]][docs-link] [![Apache 2.0 Licensed][license-image]][license-link] ![MSRV][rustc-image] [![Safety Dance][safety-image]][safety-link] [![Build Status][build-image]][build-link] Abscissa is a microframework for building Rust applications (either CLI tools or network/web services), aiming to provide a large number of features with a *minimal number of dependencies*, and with a *strong focus on security*. [Documentation][docs-link] ## Features - **command-line option parsing**: simple declarative option parser based on [clap]. The option parser in Abcissa contains numerous improvements which provide better UX and tighter integration with the other parts of the framework (e.g. overriding configuration settings using command-line options). - **components**: Abscissa uses a component architecture (similar to an ECS) for extensibility/composability, with a minimalist implementation that still provides such features such as calculating dependency ordering and providing hooks into the application lifecycle. Newly generated apps use two components by default: `terminal` and `logging`. - **configuration**: Simple parsing of TOML configurations to `serde`-parsed configuration types which can be dynamically updated at runtime. - **error handling**: unified error-handling subsystem with generic error type. - **logging**: based on the `log` to provide application-level logging. - **secrets management**: the (optional) `secrets` module includes a `Secret` type which derives serde's `Deserialize` and can be used to represent secret values parsed from configuration files or elsewhere (e.g. credentials loaded from the environment or network requests) - **terminal interactions**: support for colored terminal output (with color support autodetection). Useful for Cargo-like status messages with easy-to-use macros. ## Projects Using Abscissa - [Canister]: deployment utility for "distroless" containers/microVMs - [cargo-audit]: audit Cargo projects for security vulnerabilities - [cosmon]: observability tool for Tendermint applications - [ibc-rs]: Rust implementation of Interblockchain Communication (IBC) modules and relayer - [rustic]: fast, encrypted, and deduplicated backups - [Synchronicity]: distributed build system providing BFT proofs-of-reproducibility - [Tendermint KMS]: key management system for Tendermint applications - [Zebra]: Rust implementation of a Zcash node - [Zerostash]: Encrypted and deduplicated backups ## Crate Structure Abscissa presently consists of three crates: - [abscissa]: CLI app and application generator - `cargo install abscissa` - [abscissa_core]: main framework library - [abscissa_derive]: custom derive support - implementation detail of `abscissa_core` - [abscissa_tokio]: support for launching Tokio runtimes within Abscissa applications ## Minimum Supported Rust Version Requires Rust **1.74** or newer. ## Installation To generate a new Abscissa application, install the `abscissa` CLI utility: ```text $ cargo install abscissa ``` ## Creating a new Abscissa application The following commands will generate an Abscissa application skeleton: ```text $ cargo install abscissa $ abscissa new my_cool_app ``` The resulting app is a Cargo project. The following files are particularly noteworthy: - `src/application.rs`: Abscissa application type for your app - `src/commands*`: application entrypoint and subcommands. Make sure to check out the `start.rs` example of how to make a subcommand. - `src/config.rs`: application configuration - `src/error.rs`: error types Abscissa applications are implemented as Rust libraries, but have a `src/bin` subdirectory where the binary entrypoint lives. This means you can run the following within your newly generated application: ```text $ cargo run -- start world ``` This will invoke the `start` subcommand of your application (you might want to rename that in your app) which will print the following: ```text Hello, world! ``` You can also run the following to print basic help information: ```text $ cargo run -- --help ``` ## Status Macros ```text // Print a Cargo-like justified status to STDOUT status_ok!("Loaded", "app loaded successfully"); // Print an error message status_err!("something bad happened"); // Print an indented attribute to STDOUT status_attr_ok!("good", "yep"); // Print an error attribute to STDERR status_attr_err!("error", "yep"); ``` ## Frequently Asked Questions (FAQ) #### Q1: Why is it called "abscissa"? **A1:** The word "abscissa" is the key to the [Kryptos K2] panel. #### Q2: "Abscissa" is a hard name to remember! Got any tips? **A2**: Imagine you're A-B testing a couple of scissors... with attitude. ## Testing Framework Changes The main way to test framework changes is by generating an application with Abscissa's built-in application generator and running tests against the generated application (also rustfmt, clippy). To generate a test application and test it automatically, you can simply do: ```text $ cargo test ``` However, when debugging test failures against a generated app, it's helpful to know how to drive the app generation and testing process manually. Below are instructions on how to do so. If you've already run: ```text $ git clone https://github.com/iqlusioninc/abscissa/ ``` ...and are inside the `abscissa` directory and want to test your changes, you can generate an application by running the following command: ```text $ cargo run -- new /tmp/example_app --patch-crates-io='abscissa = { path = "$PWD" }' ``` This will generate a new Abscissa application in `/tmp/example_app` which references your local copy of Abscissa. After that, change directory to the newly generated app and run the tests to ensure things are still working (the tests, along with rustfmt and clippy are run as part of the CI process): ```text $ cd /tmp/example_app # or 'pushd /tmp/example_app' and 'popd' to return $ cargo test $ cargo fmt -- --check # generated app is expected to pass rustfmt $ cargo clippy ``` ## Code of Conduct We abide by the [Contributor Covenant][cc] and ask that you do as well. For more information, please see [CODE_OF_CONDUCT.md]. ## License The **abscissa** crate is distributed under the terms of the Apache License (Version 2.0). Copyright © 2018-2024 iqlusion Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ## Contribution If you are interested in contributing to this repository, please make sure to read the [CONTRIBUTING.md] and [CODE_OF_CONDUCT.md] files first. [//]: # (badges) [logo]: https://raw.githubusercontent.com/iqlusioninc/abscissa/main/img/abscissa.svg [crate-image]: https://img.shields.io/crates/v/abscissa_core.svg?logo=rust [crate-link]: https://crates.io/crates/abscissa_core [docs-image]: https://docs.rs/abscissa_core/badge.svg [docs-link]: https://docs.rs/abscissa_core/ [license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg [license-link]: https://github.com/iqlusioninc/abscissa/blob/main/LICENSE [rustc-image]: https://img.shields.io/badge/rustc-1.74+-blue.svg [safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg [safety-link]: https://github.com/rust-secure-code/safety-dance/ [build-image]: https://github.com/iqlusioninc/abscissa/workflows/cli/badge.svg?branch=main&event=push [build-link]: https://github.com/iqlusioninc/abscissa/actions [//]: # (crate links) [abscissa]: https://crates.io/crates/abscissa [abscissa_core]: https://crates.io/crates/abscissa_core [abscissa_derive]: https://crates.io/crates/abscissa_derive [abscissa_tokio]: https://crates.io/crates/abscissa_tokio [//]: # (general links) [cargo]: https://github.com/rust-lang/cargo [cargo features]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section [Kryptos K2]: https://en.wikipedia.org/wiki/Kryptos#Solution_of_passage_2 [cc]: https://contributor-covenant.org [CODE_OF_CONDUCT.md]: https://github.com/iqlusioninc/abscissa/blob/main/CODE_OF_CONDUCT.md [CONTRIBUTING.md]: https://github.com/iqlusioninc/abscissa/blob/main/CONTRIBUTING.md [//]: # (projects using abscissa) [Tendermint KMS]: https://github.com/iqlusioninc/tmkms [Canister]: https://github.com/iqlusioninc/canister [cargo-audit]: https://github.com/rustsec/cargo-audit [cosmon]: https://github.com/iqlusioninc/cosmon [ibc-rs]: https://github.com/informalsystems/ibc-rs [rustic]: https://github.com/rustic-rs/rustic [Synchronicity]: https://github.com/iqlusioninc/synchronicity [Zebra]: https://github.com/ZcashFoundation/zebra [Zerostash]: https://github.com/rsdy/zerostash abscissa_core-0.8.2/src/application/cell.rs000064400000000000000000000022551046102023000170160ustar 00000000000000//! Application cell: holder of application state. use super::Application; use once_cell::sync::OnceCell; use std::ops::Deref; /// Application cell: holder of application state. pub struct AppCell(OnceCell); impl AppCell { /// Create a new application cell. pub const fn new() -> AppCell { Self(OnceCell::new()) } } impl AppCell where A: Application, { /// Set the application state to the given value. /// /// This can only be performed once without causing a crash. pub(crate) fn set_once(&self, app: A) { self.0.set(app).unwrap_or_else(|_| { panic!("can't reset Abscissa application state (yet)!"); }) } } impl Deref for AppCell where A: Application, { type Target = A; #[allow(clippy::redundant_closure)] fn deref(&self) -> &A { self.0.get().unwrap_or_else(|| not_loaded()) } } /// Error handler called if `get()` is invoked before the global /// application state has been initialized. /// /// This indicates a bug in the program accessing this type. fn not_loaded() -> ! { panic!("Abscissa application state accessed before it has been initialized!") } abscissa_core-0.8.2/src/application/exit.rs000064400000000000000000000015361046102023000170510ustar 00000000000000//! Default exit handlers for Abscissa applications use super::{Application, Component}; use std::{error::Error, process}; /// Print a fatal error message and exit pub fn fatal_error(app: &impl Application, err: &dyn Error) -> ! { status_err!("{} fatal error: {}", app.name(), err); process::exit(1) } /// Exit because component startup ordering could not be determined. /// This is a barebones implementation using basic std facilities /// because it might be called before the terminal component has been /// started, and we can't use it to log errors about itself. pub(crate) fn bad_component_order(a: &dyn Component, b: &dyn Component) -> ! where A: Application, { eprintln!("*** error(abscissa): couldn't determine startup order for components:"); eprintln!(" - {:?}", a); eprintln!(" - {:?}", b); process::exit(1) } abscissa_core-0.8.2/src/application/name.rs000064400000000000000000000005701046102023000170150ustar 00000000000000//! Application name use std::fmt; /// Application name #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)] pub struct Name(pub &'static str); impl AsRef for Name { fn as_ref(&self) -> &str { self.0 } } impl fmt::Display for Name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } abscissa_core-0.8.2/src/application/state.rs000064400000000000000000000026241046102023000172170ustar 00000000000000//! Application state managed by the framework. use crate::{application::Application, component, thread}; use std::sync::RwLock; /// Error message to use for mutex error panics. const MUTEX_ERR_MSG: &str = "error acquiring mutex"; /// Framework-managed application state #[derive(Debug, Default)] pub struct State { /// Application components. components: RwLock>, /// Application paths. paths: A::Paths, /// Thread manager. threads: RwLock, } impl State where A: Application + 'static, { /// Obtain a read-only lock on the component registry. pub fn components(&self) -> component::registry::Reader<'_, A> { self.components.read().expect(MUTEX_ERR_MSG) } /// Obtain a mutable lock on the component registry. pub fn components_mut(&self) -> component::registry::Writer<'_, A> { self.components.write().expect(MUTEX_ERR_MSG) } /// Borrow the application paths. pub fn paths(&self) -> &A::Paths { &self.paths } /// Obtain a read-only lock on the thread manager. pub fn threads(&self) -> thread::manager::Reader<'_> { self.threads.read().expect(MUTEX_ERR_MSG) } /// Obtain a mutable lock on the component registry. pub fn threads_mut(&self) -> thread::manager::Writer<'_> { self.threads.write().expect(MUTEX_ERR_MSG) } } abscissa_core-0.8.2/src/application.rs000064400000000000000000000155521046102023000161030ustar 00000000000000//! Trait for representing an Abscissa application and it's lifecycle pub mod cell; pub(crate) mod exit; mod name; mod state; pub use self::{cell::AppCell, exit::fatal_error, name::Name, state::State}; use crate::{ command::Command, component::Component, config::{self, Config, Configurable}, path::{AbsPathBuf, ExePath, RootPath}, runnable::Runnable, shutdown::Shutdown, terminal::{component::Terminal, ColorChoice}, trace::{self, Tracing}, FrameworkError, FrameworkErrorKind::*, }; use std::{env, ffi::OsString, path::Path, process, vec}; /// Application types implementing this trait own global application state, /// including configuration and arbitrary other values stored within /// application components. /// /// Application lifecycle is handled by a set of components owned by types /// implementing this trait. It also ties together the following: /// /// - `Cmd`: application entrypoint /// - `Config `: application configuration /// - `Paths`: paths to various resources within the application #[allow(unused_variables)] pub trait Application: Default + Sized + 'static { /// Application (sub)command which serves as the main entry point. type Cmd: Command + Configurable + clap::Parser; /// Configuration type used by this application. type Cfg: Config; /// Paths to application resources, type Paths: Default + ExePath + RootPath; /// Run application with the given command-line arguments and running the /// appropriate `Command` type. fn run(app_cell: &'static AppCell, args: I) where I: IntoIterator, T: Into + Clone, { // Parse command line options let command = Self::Cmd::parse_args(args); // Initialize application let mut app = Self::default(); app.init(&command).unwrap_or_else(|e| fatal_error(&app, &e)); app_cell.set_once(app); // Run the command command.run(); // Exit gracefully app_cell.shutdown(Shutdown::Graceful); } /// Accessor for application configuration. fn config(&self) -> config::Reader; /// Borrow the application state. fn state(&self) -> &State; /// Register all components used by this application. fn register_components(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError>; /// Post-configuration lifecycle callback. /// /// Called regardless of whether config is loaded to indicate this is the /// time in app lifecycle when configuration would be loaded if /// possible. /// /// This method is responsible for invoking the `after_config` handlers on /// all components in the registry. This is presently done in the standard /// application template, but is not otherwise handled directly by the /// framework (as ownership precludes it). fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError>; /// Load this application's configuration and initialize its components. fn init(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> { // Create and register components with the application. // We do this first to calculate a proper dependency ordering before // application configuration is processed self.register_components(command)?; // Load configuration let config = command .config_path() .map(|path| self.load_config(&path)) .transpose()? .unwrap_or_default(); // Fire callback regardless of whether any config was loaded to // in order to signal state in the application lifecycle self.after_config(command.process_config(config)?)?; Ok(()) } /// Initialize the framework's default set of components, potentially /// sourcing terminal and tracing options from command line arguments. fn framework_components( &mut self, command: &Self::Cmd, ) -> Result>>, FrameworkError> { let terminal = Terminal::new(self.term_colors(command)); let tracing = Tracing::new(self.tracing_config(command), self.term_colors(command)) .expect("tracing subsystem failed to initialize"); Ok(vec![Box::new(terminal), Box::new(tracing)]) } /// Load configuration from the given path. /// /// Returns an error if the configuration could not be loaded. fn load_config(&mut self, path: &Path) -> Result { let canonical_path = AbsPathBuf::canonicalize(path).map_err(|e| { let path_error = PathError { name: Some(path.into()), }; FrameworkError::from(ConfigError.context(path_error)) })?; Self::Cfg::load_toml_file(canonical_path) } /// Name of this application as a string. fn name(&self) -> &'static str { Self::Cmd::name() } /// Description of this application. fn description(&self) -> &'static str { Self::Cmd::description() } /// Authors of this application. fn authors(&self) -> Vec { Self::Cmd::authors().split(':').map(str::to_owned).collect() } /// Color configuration for this application. fn term_colors(&self, command: &Self::Cmd) -> ColorChoice { ColorChoice::Auto } /// Get the tracing configuration for this application. fn tracing_config(&self, command: &Self::Cmd) -> trace::Config { trace::Config::default() } /// Shut down this application gracefully, exiting with success. fn shutdown(&self, shutdown: Shutdown) -> ! { let components = self.state().components(); if let Err(e) = components.shutdown(self, shutdown) { fatal_error(self, &e) } process::exit(0); } /// Shut down this application gracefully, exiting with user-defined exit code. fn shutdown_with_exitcode(&self, shutdown: Shutdown, exit_code: i32) -> ! { let components = self.state().components(); if let Err(e) = components.shutdown(self, shutdown) { fatal_error(self, &e) } process::exit(exit_code); } } /// Boot the given application, parsing subcommand and options from /// command-line arguments, and terminating when complete. pub fn boot(app_cell: &'static AppCell) -> ! { let args = env::args_os(); boot_with_args(app_cell, args) } /// Boot the given application, parsing subcommand and options from /// the provided iterator, and terminating when complete. /// /// This is useful if you want to pre-process the arguments, /// e.g. perform glob expansion on Windows. Otherwise use [boot]. pub fn boot_with_args(app_cell: &'static AppCell, args: I) -> ! where A: Application, I: IntoIterator, T: Into + Clone, { A::run(app_cell, args); process::exit(0); } abscissa_core-0.8.2/src/command.rs000064400000000000000000000032561046102023000152140ustar 00000000000000//! Application (sub)command(s), i.e. app entry points #[doc(hidden)] pub use abscissa_derive::Command; use crate::{runnable::Runnable, terminal}; use clap::{FromArgMatches, Parser}; use std::{env, ffi::OsString, fmt::Debug}; use termcolor::ColorChoice; /// Subcommand of an application: derives or otherwise implements the `Options` /// trait, but also has a `run()` method which can be used to invoke the given /// (sub)command. pub trait Command: Debug + FromArgMatches + Runnable { /// Name of this program as a string fn name() -> &'static str; /// Description of this program fn description() -> &'static str; /// Authors of this program fn authors() -> &'static str; /// Parse command-line arguments from an iterator fn parse_args(into_args: I) -> Self where Self: Parser, I: IntoIterator, T: Into + Clone, { let args: Vec = into_args.into_iter().map(|s| s.into()).collect(); Self::try_parse_from(args.as_slice()).unwrap_or_else(|err| { terminal::init(ColorChoice::Auto); err.exit() }) } /// Parse command-line arguments from the environment fn parse_env_args() -> Self where Self: Parser, { Self::parse_args(env::args()) } } #[cfg(test)] mod tests { use crate::{Command, Runnable}; use clap::Parser; #[derive(Command, Debug, Parser)] pub struct DummyCommand {} impl Runnable for DummyCommand { fn run(&self) { panic!("unimplemented"); } } #[test] fn derived_command_test() { assert_eq!(DummyCommand::name(), "abscissa_core"); } } abscissa_core-0.8.2/src/component/handle.rs000064400000000000000000000020521046102023000170240ustar 00000000000000//! Component handles: opaque references to registered components use super::{id::Id, registry::Index}; use std::fmt; /// Component handles are references to components which have been registered /// with a `component::Registry`. /// /// However, unlike normal Rust references, component handles are a "weak" /// reference which is not checked by the borrow checker. This allows for /// complex reference graphs which are otherwise inexpressible in Rust. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct Handle { /// Component name name: Id, /// Registry index pub(super) index: Index, } impl Handle { /// Create a new handle from a component's name and index pub(crate) fn new(name: Id, index: Index) -> Self { Self { name, index } } /// Get the identifier of the component this handle points to pub fn id(self) -> Id { self.name } } impl fmt::Debug for Handle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Handle({})", self.id().as_ref()) } } abscissa_core-0.8.2/src/component/id.rs000064400000000000000000000015341046102023000161710ustar 00000000000000//! Component identifiers //! //! By convention these are Rust paths to the component types // TODO(tarcieri): enforce this convention via e.g. custom derive? use std::fmt; /// Identifier for an individual component /// /// This should ideally match the Rust path name to the corresponding type. // TODO(tarcieri): obtain this automatically via `std::module_path`? #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)] pub struct Id(&'static str); impl Id { /// Create a new component identifier // TODO(tarcieri): make this method private in the future pub const fn new(id: &'static str) -> Id { Id(id) } } impl AsRef for Id { fn as_ref(&self) -> &str { self.0 } } impl fmt::Display for Id { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } abscissa_core-0.8.2/src/component/registry.rs000064400000000000000000000203411046102023000174420ustar 00000000000000//! Abscissa's component registry use super::{handle::Handle, id::Id, Component}; use crate::{ application::{self, Application}, shutdown::Shutdown, FrameworkError, FrameworkErrorKind::ComponentError, Map, }; use std::{any::TypeId, borrow::Borrow, slice, sync}; /// Iterator over the components in the registry. pub type Iter<'a, A> = slice::Iter<'a, Box>>; /// Mutable iterator over the components in the registry. pub type IterMut<'a, A> = slice::IterMut<'a, Box>>; /// Reader guard for the registry. pub type Reader<'a, A> = sync::RwLockReadGuard<'a, Registry>; /// Writer guard for the registry. pub type Writer<'a, A> = sync::RwLockWriteGuard<'a, Registry>; /// Index of component identifiers to their arena locations. type IdMap = Map; /// Index of component type IDs to their arena locations. type TypeMap = Map; /// Index type providing efficient access to a particular component. #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub struct Index(usize); /// The component registry provides a system for runtime registration of /// application components which can interact with each other dynamically. /// /// Components are sorted according to a dependency ordering, started /// in-order, and at application termination time, shut down in reverse order. #[derive(Debug, Default)] pub struct Registry { /// Generational arena of registered components components: Vec>>, /// Map of component identifiers to their indexes id_map: IdMap, /// Map of component types to their identifiers type_map: TypeMap, } impl Registry where A: Application + 'static, { /// Register components, determining their dependency order pub fn register(&mut self, components: I) -> Result<(), FrameworkError> where I: IntoIterator>>, { // TODO(tarcieri): flexible runtime registration? ensure!( self.components.is_empty(), ComponentError, "no support for registering additional components (yet)" ); let mut components = components.into_iter().collect::>(); components.sort_by(|a, b| { a.partial_cmp(b) .unwrap_or_else(|| application::exit::bad_component_order(a.borrow(), b.borrow())) }); for component in components { self.register_component(component)?; } Ok(()) } /// Callback fired by application when configuration has been loaded pub fn after_config(&mut self, config: &A::Cfg) -> Result<(), FrameworkError> { let mut component_indexes: Vec<(Index, Vec)> = vec![]; for (index, component) in self.components.iter_mut().enumerate() { // Fire the `after_config` callback for each subcomponent. // // Note that these are fired for *all* components prior to subcomponent registration component.after_config(config)?; let mut dep_indexes = vec![]; for id in component.dependencies() { if let Some(index) = self.id_map.get(id) { dep_indexes.push(*index); } else { fail!(ComponentError, "unregistered dependency ID: {}", id); } } component_indexes.push((Index(index), dep_indexes)); } // Fire the `register_dependency` callbacks for each component's dependencies for (component_index, dep_indexes) in component_indexes { for dep_index in dep_indexes { if let (Some(component), Some(dep)) = self.get2_mut(component_index, dep_index) { let dep_handle = Handle::new(dep.id(), dep_index); component.register_dependency(dep_handle, dep.as_mut())?; } else { // In theory we just looked all of these up and they should always be valid unreachable!(); } } } Ok(()) } /// Get the number of currently registered components pub fn len(&self) -> usize { self.components.len() } /// Is the registry empty? pub fn is_empty(&self) -> bool { self.components.is_empty() } /// Get a component reference by its handle pub fn get(&self, handle: Handle) -> Option<&dyn Component> { self.components.get(handle.index.0).map(AsRef::as_ref) } /// Get a mutable component reference by its handle pub fn get_mut(&mut self, handle: Handle) -> Option<&mut (dyn Component + 'static)> { self.components.get_mut(handle.index.0).map(AsMut::as_mut) } /// Get a component's handle by its ID pub fn get_handle_by_id(&self, id: Id) -> Option { Some(Handle::new(id, *self.id_map.get(&id)?)) } /// Get the handle for the given component, if it's registered pub fn get_handle(&self, component: &dyn Component) -> Option { self.get_handle_by_id(component.id()) } /// Get a component ref by its ID pub fn get_by_id(&self, id: Id) -> Option<&dyn Component> { self.get(self.get_handle_by_id(id)?) } /// Get a mutable component ref by its ID pub fn get_mut_by_id(&mut self, id: Id) -> Option<&mut (dyn Component + 'static)> { self.get_mut(self.get_handle_by_id(id)?) } /// Iterate over the components. pub fn iter(&self) -> Iter<'_, A> { self.components.iter() } /// Iterate over the components mutably. pub fn iter_mut(&mut self) -> IterMut<'_, A> { self.components.iter_mut() } /// Shutdown components (in the reverse order they were started) pub fn shutdown(&self, app: &A, shutdown: Shutdown) -> Result<(), FrameworkError> { for component in self.components.iter().rev() { component.before_shutdown(shutdown)?; } Ok(()) } /// Get a component reference by its type pub fn get_downcast_ref(&self) -> Option<&C> where C: Component, { let index = *self.type_map.get(&TypeId::of::())?; self.components .get(index.0) .and_then(|box_component| (*(*box_component)).as_any().downcast_ref()) } /// Get a mutable component reference by its type pub fn get_downcast_mut(&mut self) -> Option<&mut C> where C: Component, { let index = *self.type_map.get(&TypeId::of::())?; self.components .get_mut(index.0) .and_then(|box_component| (*(*box_component)).as_mut_any().downcast_mut()) } /// Register an individual component. /// /// This is an internal method used by `Registry::register`. /// It shouldn't be exposed through the public API without careful /// consideration, as it's not yet designed to be used outside of /// that particular context. fn register_component( &mut self, component: Box>, ) -> Result<(), FrameworkError> { let id = component.id(); let version = component.version(); let type_id = (*component).type_id(); ensure!( !self.id_map.contains_key(&id) && !self.type_map.contains_key(&type_id), ComponentError, "duplicate component registration: {}", id ); let index = Index(self.components.len()); self.components.push(component); // Index component by ID and type assert!(self.id_map.insert(id, index).is_none()); assert!(self.type_map.insert(type_id, index).is_none()); debug!("registered component: {} (v{})", id, version); Ok(()) } /// Borrow two components mutably (i.e. borrow splitting) #[allow(clippy::type_complexity)] fn get2_mut( &mut self, a: Index, b: Index, ) -> ( Option<&mut Box>>, Option<&mut Box>>, ) { let (a_slice, b_slice) = if b.0 <= self.components.len() { self.components.split_at_mut(b.0) } else { (self.components.as_mut(), Default::default()) }; (a_slice.get_mut(a.0), b_slice.first_mut()) } } abscissa_core-0.8.2/src/component.rs000064400000000000000000000073621046102023000156020ustar 00000000000000//! Application components: extensions/plugins for Abscissa applications. //! //! See docs on the `Component` trait for more information. #![allow(unused_variables)] mod handle; mod id; pub mod registry; pub use self::{handle::Handle, id::Id, registry::Registry}; pub use abscissa_derive::Component; use crate::{application::Application, shutdown::Shutdown, FrameworkError, Version}; use std::{any::Any, cmp::Ordering, fmt::Debug, slice::Iter}; /// Application components. /// /// Components are Abscissa's primary extension mechanism, and are aware of /// the application lifecycle. They are owned by the application as boxed trait /// objects in a runtime type registry which is aware of a dependency ordering /// and can (potentially in the future) support runtime reinitialization. /// /// During application initialization, callbacks are sent to all components /// upon events like application configuration being loaded. The /// `register_dependency` callback is called for each dependency returned /// by the `dependencies` method. /// /// Additionally, they receive a callback prior to application shutdown. /// /// ## Custom Derive /// /// The main intended way to impl this trait is by using the built-in custom /// derive functionality. /// /// ```rust /// use abscissa_core::Component; /// /// #[derive(Component, Debug)] /// pub struct MyComponent {} /// ``` /// /// This will automatically implement the entire trait for you. pub trait Component: AsAny + Debug + Send + Sync where A: Application, { /// Identifier for this component. /// /// These are the Rust path (e.g. `crate_name:foo::Foo`) by convention. fn id(&self) -> Id; /// Version of this component fn version(&self) -> Version; /// Lifecycle event called when application configuration should be loaded /// if it were possible. fn after_config(&mut self, config: &A::Cfg) -> Result<(), FrameworkError> { Ok(()) } /// Names of the components this component depends on. /// /// After this app's `after_config` callback is fired, the /// `register_dependency` callback below will be fired for each of these. fn dependencies(&self) -> Iter<'_, Id> { [].iter() } /// Register a dependency of this component (a.k.a. "dependency injection") fn register_dependency( &mut self, handle: Handle, dependency: &mut dyn Component, ) -> Result<(), FrameworkError> { unimplemented!(); } /// Perform any tasks which should occur before the app exits fn before_shutdown(&self, kind: Shutdown) -> Result<(), FrameworkError> { Ok(()) } } impl PartialEq for Box> where A: Application, { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } impl PartialOrd for Box> where A: Application, { fn partial_cmp(&self, other: &Self) -> Option { if other.dependencies().any(|dep| *dep == self.id()) { if self.dependencies().any(|dep| *dep == other.id()) { None } else { Some(Ordering::Greater) } } else if self.dependencies().any(|dep| *dep == other.id()) { Some(Ordering::Less) } else { Some(Ordering::Equal) } } } /// Dynamic type helper trait // TODO(tarcieri): eliminate this trait or hide it from the public API pub trait AsAny: Any { /// Borrow this concrete type as a `&dyn Any` fn as_any(&self) -> &dyn Any; /// Borrow this concrete type as a `&mut dyn Any` fn as_mut_any(&mut self) -> &mut dyn Any; } impl AsAny for T where T: Any, { fn as_any(&self) -> &dyn Any { self } fn as_mut_any(&mut self) -> &mut dyn Any { self } } abscissa_core-0.8.2/src/config/cell.rs000064400000000000000000000022631046102023000157570ustar 00000000000000//! Configuration cell: holder of application configuration. use super::{Config, Reader}; use arc_swap::ArcSwapOption; use std::sync::Arc; /// Configuration cell: holder of application configuration. #[derive(Debug, Default)] pub struct CfgCell { /// Inner configuration state inner: ArcSwapOption, } impl CfgCell where C: Config, { /// Set the application configuration to the given value. /// /// This can only be performed once without causing a crash. pub fn set_once(&self, config: C) { let old_config = self.inner.swap(Some(Arc::new(config))); if old_config.is_some() { panic!("can't reload Abscissa application config (yet)!"); } } /// Read the current configuration. #[allow(clippy::redundant_closure)] pub fn read(&self) -> Reader { self.inner.load_full().unwrap_or_else(|| not_loaded()) } } /// Error handler called if `get()` is invoked before the global /// application state has been initialized. /// /// This indicates a bug in the program accessing this type. fn not_loaded() -> ! { panic!("Abscissa application state accessed before it has been initialized!") } abscissa_core-0.8.2/src/config/configurable.rs000064400000000000000000000011151046102023000174730ustar 00000000000000//! Configuration loader use super::Config; use crate::FrameworkError; use std::path::PathBuf; /// Command type with which a configuration file is associated pub trait Configurable { /// Path to the command's configuration file. Returns an error by default. fn config_path(&self) -> Option { None } /// Process the configuration after it has been loaded, potentially /// modifying it or returning an error if options are incompatible fn process_config(&self, config: Cfg) -> Result { Ok(config) } } abscissa_core-0.8.2/src/config/overrides.rs000064400000000000000000000014521046102023000170410ustar 00000000000000//! Override values in the configuration file with command-line options use crate::{Command, Config, FrameworkError}; /// Use options from the given `Command` to override settings in the config. pub trait Override: Command { /// Process the given command line options, overriding settings from /// a configuration file using explicit flags taken from command-line /// arguments. /// /// This provides a canonical way to interpret global configuration /// settings when dealing with both a config file and options passed /// on the command line, and a unified way of accessing this information /// from components or in the application: from the global config. fn override_config(&self, config: Cfg) -> Result { Ok(config) } } abscissa_core-0.8.2/src/config.rs000064400000000000000000000033211046102023000150340ustar 00000000000000//! Support for managing global configuration, as well as loading it from TOML. mod cell; mod configurable; mod overrides; pub use self::{cell::CfgCell, configurable::Configurable, overrides::Override}; use crate::{ fs::File, path::AbsPath, FrameworkError, FrameworkErrorKind::{ConfigError, IoError, PathError}, }; use serde::de::DeserializeOwned; use std::{fmt::Debug, io::Read}; /// Configuration reader. #[cfg(feature = "application")] pub type Reader = std::sync::Arc; /// Trait for Abscissa configuration data structures. pub trait Config: Debug + Default + DeserializeOwned { /// Load the configuration from the given TOML string. fn load_toml(toml_string: impl AsRef) -> Result; /// Load the global configuration from the TOML file at the given path. /// If an error occurs reading or parsing the file, print it out and exit. fn load_toml_file(path: impl AsRef) -> Result; } impl Config for C where C: Debug + Default + DeserializeOwned, { fn load_toml(toml_string: impl AsRef) -> Result { Ok(toml::from_str(toml_string.as_ref())?) } fn load_toml_file(path: impl AsRef) -> Result { let mut file = File::open(path.as_ref().as_path()).map_err(|e| { let io_error = IoError.context(e); let path_error = PathError { name: Some(path.as_ref().as_path().into()), } .context(io_error); ConfigError.context(path_error) })?; let mut toml_string = String::new(); file.read_to_string(&mut toml_string)?; Self::load_toml(toml_string) } } abscissa_core-0.8.2/src/error/context.rs000064400000000000000000000037761046102023000164220ustar 00000000000000//! Error contexts use super::BoxError; use backtrace::Backtrace; use std::fmt::{self, Debug, Display}; /// Error context #[derive(Debug)] pub struct Context where Kind: Clone + Debug + Display + Eq + PartialEq + Into, { /// Kind of error kind: Kind, /// Backtrace where error occurred backtrace: Option, /// Source of the error source: Option, } impl Context where Kind: Clone + Debug + Display + Eq + PartialEq + Into, { /// Create a new error context pub fn new(kind: Kind, source: Option) -> Self { let backtrace = Some(Backtrace::new_unresolved()); Context { kind, backtrace, source, } } /// Get the kind of error pub fn kind(&self) -> &Kind { &self.kind } /// Get the backtrace associated with this error (if available) pub fn backtrace(&self) -> Option<&Backtrace> { self.backtrace.as_ref() } /// Extract the backtrace from the context, allowing it to be resolved. pub fn into_backtrace(self) -> Option { self.backtrace } } impl Display for Context where Kind: Clone + Debug + Display + Eq + PartialEq + Into, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", &self.kind)?; if let Some(ref source) = self.source { write!(f, ": {}", source)?; } Ok(()) } } impl From for Context where Kind: Clone + Debug + Display + Eq + PartialEq + Into, { fn from(kind: Kind) -> Context { Self::new(kind, None) } } impl std::error::Error for Context where Kind: Clone + Debug + Display + Eq + PartialEq + Into, { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.source .as_ref() .map(|source| source.as_ref() as &(dyn std::error::Error + 'static)) } } abscissa_core-0.8.2/src/error/framework.rs000064400000000000000000000061541046102023000167240ustar 00000000000000//! Framework error types use super::{context::Context, BoxError}; use std::{ fmt::{self, Display}, io, ops::Deref, path::PathBuf, }; /// Abscissa-internal framework errors #[derive(Debug)] pub struct FrameworkError(Box>); impl Deref for FrameworkError { type Target = Context; fn deref(&self) -> &Context { &self.0 } } impl Display for FrameworkError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl From> for FrameworkError { fn from(context: Context) -> Self { FrameworkError(Box::new(context)) } } impl std::error::Error for FrameworkError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() } } /// Types of errors which occur internally within the framework #[derive(Clone, Debug, Eq, PartialEq)] pub enum FrameworkErrorKind { /// Errors relating to components #[cfg(feature = "application")] ComponentError, /// Error reading configuration file ConfigError, /// I/O operation failed IoError, /// Couldn't parse the given value ParseError, /// Errors associated with filesystem paths PathError { /// Name of the affected path (if applicable) name: Option, }, /// Errors occurring in subprocess ProcessError, /// Errors involving multithreading ThreadError, /// Timeout performing operation TimeoutError, } impl FrameworkErrorKind { /// Create an error context from this error pub fn context(self, source: impl Into) -> Context { Context::new(self, Some(source.into())) } /// Get a message to display for this error pub fn msg(&self) -> &'static str { match self { #[cfg(feature = "application")] FrameworkErrorKind::ComponentError => "component error", FrameworkErrorKind::ConfigError => "config error", FrameworkErrorKind::IoError => "I/O operation failed", FrameworkErrorKind::ParseError => "parse error", FrameworkErrorKind::PathError { .. } => "path error", FrameworkErrorKind::ProcessError => "subprocess error", FrameworkErrorKind::ThreadError => "thread error", FrameworkErrorKind::TimeoutError => "operation timed out", } } } impl Display for FrameworkErrorKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.msg())?; if let FrameworkErrorKind::PathError { name: Some(name) } = self { write!(f, ": {}", name.display())?; } Ok(()) } } impl std::error::Error for FrameworkErrorKind {} impl From for FrameworkError { fn from(err: io::Error) -> Self { FrameworkErrorKind::IoError.context(err).into() } } #[cfg(feature = "toml")] impl From for FrameworkError { fn from(err: toml::de::Error) -> Self { FrameworkErrorKind::ParseError.context(err).into() } } abscissa_core-0.8.2/src/error/macros.rs000064400000000000000000000041521046102023000162070ustar 00000000000000//! Error-handling macros for the `abscissa` framework //! //! This crate defines two error handling macros designed to produce formatted //! error messages from error kind enums that implement the `Fail` trait: //! //! * `err!(kind, description)` creates a new `Error` with the given //! description. If additional parameters are given, `description` is treated as //! a format string, e.g. `err!(kind, "something went wrong: {}", &wrongness)`. //! * `fail!(kind, description)` creates a new `Error` and returns it. /// Create a new error (of a given kind) with a formatted message #[macro_export] macro_rules! format_err { ($kind:expr, $msg:expr) => { $kind.context($crate::error::Message::new($msg)) }; ($kind:expr, $fmt:expr, $($arg:tt)+) => { format_err!($kind, &format!($fmt, $($arg)+)) }; } /// Create and return an error with a formatted message #[macro_export] macro_rules! fail { ($kind:expr, $msg:expr) => { return Err(format_err!($kind, $msg).into()) }; ($kind:expr, $fmt:expr, $($arg:tt)+) => { fail!($kind, &format!($fmt, $($arg)+)) }; } /// Ensure a condition holds, returning an error if it doesn't (ala assert) #[macro_export] macro_rules! ensure { ($cond:expr, $kind:expr, $msg:expr) => { if !($cond) { return Err(format_err!($kind, $msg).into()); } }; ($cond:expr, $kind:expr, $fmt:expr, $($arg:tt)+) => { ensure!($cond, $kind, format!($fmt, $($arg)+)) }; } /// Terminate the application with a fatal error, running Abscissa's shutdown hooks. /// /// This macro is useful in cases where you don't have a particular error type /// you'd like to use when exiting but would like to have a formatted error /// message. If you do have a suitable error type, use `fatal_error!()` instead. /// /// Takes the same arguments as `format!()`. #[macro_export] macro_rules! fatal { ($app:expr, $msg:expr) => { $crate::application::exit::fatal_error($app, $crate::error::Message::new($msg)) }; ($app:expr, $fmt:expr, $($arg:tt)+) => { fatal!($app, format!($fmt, $($arg:tt)+)) }; } abscissa_core-0.8.2/src/error/message.rs000064400000000000000000000016611046102023000163510ustar 00000000000000//! Error messages use std::{ error::Error, fmt::{self, Display}, string::ToString, }; /// Error message type: provide additional context with a string. /// /// This is generally discouraged whenever possible as it will complicate /// future I18n support. However, it can be useful for things with /// language-independent string representations for error contexts. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Message(String); impl Message { /// Create a new error message pub fn new(msg: impl ToString) -> Self { Message(msg.to_string()) } } impl AsRef for Message { fn as_ref(&self) -> &str { self.0.as_ref() } } impl Display for Message { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_ref()) } } impl Error for Message {} impl From for Message { fn from(string: String) -> Message { Message(string) } } abscissa_core-0.8.2/src/error.rs000064400000000000000000000005441046102023000147240ustar 00000000000000//! Error types used by this crate #[macro_use] pub mod macros; pub mod context; pub mod framework; pub mod message; pub use self::{context::Context, message::Message}; /// Box containing a thread-safe + `'static` error suitable for use as a /// as an `std::error::Error::source` pub type BoxError = Box; abscissa_core-0.8.2/src/lib.rs000064400000000000000000000034471046102023000143460ustar 00000000000000#![doc = include_str!("../README.md")] #![doc( html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/abscissa/main/img/abscissa-sq.svg" )] #![forbid(unsafe_code)] #![warn( missing_docs, rust_2018_idioms, unused_lifetimes, unused_qualifications )] /// Abscissa version pub const VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg(feature = "trace")] #[allow(unused_imports)] #[macro_use] pub extern crate tracing; // Modules with macro exports #[macro_use] pub mod error; #[cfg(feature = "terminal")] #[macro_use] pub mod terminal; // Other modules #[cfg(feature = "application")] pub mod application; #[cfg(feature = "options")] pub mod command; #[cfg(feature = "application")] pub mod component; #[cfg(feature = "config")] pub mod config; pub mod path; #[cfg(feature = "application")] pub mod prelude; mod runnable; #[cfg(feature = "application")] mod shutdown; #[cfg(feature = "testing")] pub mod testing; pub mod thread; #[cfg(feature = "trace")] pub mod trace; // Re-exports pub use crate::{ error::framework::{FrameworkError, FrameworkErrorKind}, runnable::{Runnable, RunnableMut}, }; pub use std::collections::{btree_map as map, btree_set as set, BTreeMap as Map}; #[cfg(feature = "application")] pub use crate::{ application::{boot, Application}, component::Component, shutdown::Shutdown, }; #[cfg(feature = "config")] pub use crate::config::{Config, Configurable}; #[cfg(feature = "options")] pub use crate::{command::Command, path::StandardPaths}; // Re-exported modules/types from third-party crates #[cfg(feature = "options")] pub use clap; pub use fs_err as fs; #[cfg(feature = "secrets")] pub use secrecy as secret; #[cfg(feature = "secrets")] pub use secrecy::{SecretBox, SecretSlice, SecretString}; #[cfg(feature = "application")] pub use semver::Version; abscissa_core-0.8.2/src/path.rs000064400000000000000000000057521046102023000145350ustar 00000000000000//! Paths for resources used by the application. //! //! Implemented as a trait to support extensibility and customizability, but //! with a a set of framework conventions. pub use canonical_path::{CanonicalPath as AbsPath, CanonicalPathBuf as AbsPathBuf}; // Just in case anyone gets confused why `Path` is private pub use std::path::{Path, PathBuf}; use crate::FrameworkError; /// Name of the application's secrets directory pub(crate) const SECRETS_DIR: &str = "secrets"; /// Path to the application's executable. pub trait ExePath { /// Get the path to the application's executable fn exe(&self) -> &AbsPath; } /// Path to application's root directory pub trait RootPath { /// Get the path to the application's root directory fn root(&self) -> &AbsPath; } /// Path to the application's secrets directory pub trait SecretsPath { /// Get the path to the application's secrets directory fn secrets(&self) -> &AbsPath; } /// Standard set of "happy paths" used by Abscissa applications. /// /// These are not yet finalized, but provide a standard application layout /// (further expressed in the template) which future Abscissa /// components/extensions should seek to adhere to. #[derive(Clone, Debug)] pub struct StandardPaths { /// Path to the application's executable. exe: AbsPathBuf, /// Path to the application's root directory root: AbsPathBuf, /// Path to the application's secrets secrets: Option, } impl StandardPaths { /// Compute paths to application resources from the path of the /// application's executable: /// /// - `./` (root): application root directory /// - `./{{~name~}}` (bin): application executable path /// - `./secrets` (secrets): location of files containing app's secrets fn from_exe_path

(exe_path: P) -> Result where P: Into, { let exe = exe_path.into(); let root = exe.parent()?; let secrets = root.join(SECRETS_DIR).ok(); Ok(StandardPaths { exe, root, secrets }) } } impl Default for StandardPaths { // TODO(tarcieri): better error handling for canonicalization failures fn default() -> Self { let exe_path = canonical_path::current_exe().unwrap_or_else(|e| { panic!("error canonicalizing application path: {}", e); }); Self::from_exe_path(exe_path).unwrap_or_else(|e| { panic!("error computing application paths: {}", e); }) } } impl ExePath for StandardPaths { fn exe(&self) -> &AbsPath { self.exe.as_ref() } } impl RootPath for StandardPaths { fn root(&self) -> &AbsPath { self.root.as_ref() } } impl SecretsPath for StandardPaths { fn secrets(&self) -> &AbsPath { self.secrets .as_ref() .unwrap_or_else(|| { // TODO(tarcieri): better error handling for this case panic!("secrets directory does not exist"); }) .as_ref() } } abscissa_core-0.8.2/src/prelude.rs000064400000000000000000000006171046102023000152340ustar 00000000000000//! Core prelude: imported in every application's `prelude.rs` /// Commonly used Abscissa traits pub use crate::{Application, Command, Runnable}; /// Error macros pub use crate::{ensure, fail, fatal, format_err}; /// Tracing macros pub use crate::tracing::{debug, error, event, info, span, trace, warn, Level}; /// Status macros pub use crate::{status_err, status_info, status_ok, status_warn}; abscissa_core-0.8.2/src/runnable.rs000064400000000000000000000033411046102023000153770ustar 00000000000000//! `Runnable` trait. #[doc(hidden)] pub use abscissa_derive::Runnable; /// `Runnable` is a common trait for things which can be run without any /// arguments. /// /// Its primary intended purpose is for use in conjunction with `Command`. pub trait Runnable { /// Run this `Runnable` fn run(&self); } /// `RunnableMut` is a `Runnable` that takes a mutable reference to `self`. pub trait RunnableMut { /// Run this `RunnableMut` fn run(&mut self); } impl Runnable for Box { fn run(&self) { self(); } } impl RunnableMut for Box { fn run(&mut self) { self(); } } #[cfg(test)] mod tests { use crate::Runnable; use std::sync::Mutex; #[allow(dead_code)] #[derive(Runnable)] enum TestEnum { A(VariantA), B(VariantB), } #[allow(dead_code)] struct VariantA {} impl Runnable for VariantA { fn run(&self) { panic!("don't call this!") } } #[derive(Default)] struct VariantB { called: Mutex, } impl VariantB { fn was_called(&self) -> bool { let called = self.called.lock().unwrap(); *called } } impl Runnable for VariantB { fn run(&self) { let mut called = self.called.lock().unwrap(); *called = true; } } #[test] fn custom_derive_test() { let variant_b = VariantB::default(); assert!(!variant_b.was_called()); let ex = TestEnum::B(variant_b); ex.run(); let variant_b = match ex { TestEnum::A(_) => panic!("this shouldn't be!"), TestEnum::B(b) => b, }; assert!(variant_b.was_called()); } } abscissa_core-0.8.2/src/shutdown.rs000064400000000000000000000017121046102023000154440ustar 00000000000000//! Application shutdown support /// Types of shutdown recognized by Abscissa #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub enum Shutdown { /// Graceful shutdowns may take prolonged periods of time, allowing /// components to take their time to ensure shutdowns occur cleanly /// (e.g. draining currently active traffic rather than closing sockets) Graceful, /// Forced shutdowns indicate the program's user has requested it terminate /// immediately. Components receiving this kind of shutdown should do only /// critical cleanup tasks which can be completed quickly. Forced, /// This shutdown type is a "best effort" to communicate that the /// application has suffered from a critical error and is in the process /// of exiting. Components may use this to do crash reporting prior /// to the application exit, as well as any other cleanup deemed suitable /// within a crashing application. Crash, } abscissa_core-0.8.2/src/terminal/component.rs000064400000000000000000000012111046102023000174000ustar 00000000000000//! Terminal component use crate::Component; use termcolor::ColorChoice; /// Abscissa terminal subsystem component #[derive(Component, Debug)] #[component(core)] pub struct Terminal {} impl Terminal { /// Create a new [`Terminal`] component with the given [`ColorChoice`] pub fn new(color_choice: ColorChoice) -> Terminal { // TODO(tarcieri): handle terminal reinit (without panicking) super::init(color_choice); if color_choice != ColorChoice::Never { // TODO(tarcieri): avoid panicking here color_eyre::install().expect("couldn't install color-eyre"); } Self {} } } abscissa_core-0.8.2/src/terminal/status.rs000064400000000000000000000156131046102023000167340ustar 00000000000000//! Terminal status handling. //! //! Presently provides a Cargo-like visual style. Hopefully in future versions //! this can be made configurable. //! //! # `status_ok!`: Successful status messages //! //! ```ignore //! // Print a Cargo-like justified status to STDOUT //! status_ok!("Loaded", "app loaded successfully"); //! ``` //! //! # `status_err!`: Error messages //! //! ```ignore //! // Print an error message //! status_err!("something bad happened"); //! ``` //! //! # `status_attr_ok!`: Successful attributes //! //! ```ignore //! // Print an indented attribute to STDOUT //! status_attr_ok!("good", "yep"); //! ``` //! //! # `status_attr_error!`: Error attributes //! //! ```ignore //! // Print an error attribute to STDERR //! status_attr_err!("error", "yep"); //! ``` use super::{stderr, stdout}; use crate::FrameworkError; use std::io::Write; use termcolor::{Color, ColorSpec, StandardStream, WriteColor}; /// Print a success status message (in green if colors are enabled) /// /// ```ignore /// // Print a Cargo-like justified status to STDOUT /// status_ok!("Loaded", "app loaded successfully"); /// ``` #[macro_export] macro_rules! status_ok { ($status:expr, $msg:expr) => { $crate::terminal::status::Status::new() .justified() .bold() .color($crate::terminal::Color::Green) .status($status) .print_stderr($msg) .unwrap(); }; ($status:expr, $fmt:expr, $($arg:tt)+) => { $crate::status_ok!($status, format!($fmt, $($arg)+)); }; } /// Print an informational status message (in cyan if colors are enabled) /// /// ```ignore /// // Print a Cargo-like justified status to STDOUT /// status_info!("Info", "you may care to know about"); /// ``` #[macro_export] macro_rules! status_info { ($status:expr, $msg:expr) => { $crate::terminal::status::Status::new() .justified() .bold() .color($crate::terminal::Color::Cyan) .status($status) .print_stderr($msg) .unwrap(); }; ($status:expr, $fmt:expr, $($arg:tt)+) => { $crate::status_info!($status, format!($fmt, $($arg)+)); }; } /// Print a warning status message (in yellow if colors are enabled) /// /// ```ignore /// // Print a Cargo-like justified status to STDOUT /// status_warn!("heads up, there's something you should know"); /// ``` #[macro_export] macro_rules! status_warn { ($msg:expr) => { $crate::terminal::status::Status::new() .bold() .color($crate::terminal::Color::Yellow) .status("warning:") .print_stderr($msg) .unwrap(); }; ($fmt:expr, $($arg:tt)+) => { $crate::status_warn!(format!($fmt, $($arg)+)); }; } /// Print an error message (in red if colors are enabled) /// /// ```ignore /// // Print an error message /// status_err!("something bad happened"); /// ``` #[macro_export] macro_rules! status_err { ($msg:expr) => { $crate::terminal::status::Status::new() .bold() .color($crate::terminal::Color::Red) .status("error:") .print_stderr($msg) .unwrap(); }; ($fmt:expr, $($arg:tt)+) => { $crate::status_err!(format!($fmt, $($arg)+)); }; } /// Print a tab-delimited status attribute (in green if colors are enabled) /// /// ```ignore /// // Print an indented attribute to STDOUT /// status_attr_ok!("good", "yep"); /// ``` #[macro_export] macro_rules! status_attr_ok { ($attr:expr, $msg:expr) => { // TODO(tarcieri): hax... use a better format string? let attr_delimited = if $attr.len() >= 7 { format!("{}:", $attr) } else { format!("{}:\t", $attr) }; $crate::terminal::status::Status::new() .bold() .color($crate::terminal::Color::Green) .status(attr_delimited) .print_stdout($msg) .unwrap(); }; ($attr: expr, $fmt:expr, $($arg:tt)+) => { $crate::status_attr_ok!($attr, format!($fmt, $($arg)+)); } } /// Print a tab-delimited status attribute (in red if colors are enabled) /// /// ```ignore /// // Print an error attribute to STDERR /// status_attr_err!("error", "yep"); /// ``` #[macro_export] macro_rules! status_attr_err { ($attr:expr, $msg:expr) => { // TODO(tarcieri): hax... use a better format string? let attr_delimited = if $attr.len() >= 7 { format!("{}:", $attr) } else { format!("{}:\t", $attr) }; $crate::terminal::status::Status::new() .bold() .color($crate::terminal::Color::Red) .status(attr_delimited) .print_stdout($msg) .unwrap(); }; ($attr: expr, $fmt:expr, $($arg:tt)+) => { $crate::status_attr_err!($attr, format!($fmt, $($arg)+)); } } /// Status message builder #[derive(Clone, Debug, Default)] pub struct Status { /// Should the status be justified? justified: bool, /// Should colors be bold? bold: bool, /// Color in which status should be displayed color: Option, /// Prefix of the status message (e.g. `Success`) status: Option, } impl Status { /// Create a new status message with default settings pub fn new() -> Self { Self::default() } /// Justify status on display pub fn justified(mut self) -> Self { self.justified = true; self } /// Make colors bold pub fn bold(mut self) -> Self { self.bold = true; self } /// Set the colors used to display this message pub fn color(mut self, c: Color) -> Self { self.color = Some(c); self } /// Set a status message to display pub fn status(mut self, msg: S) -> Self where S: ToString, { self.status = Some(msg.to_string()); self } /// Print the given message to stdout pub fn print_stdout(self, msg: S) -> Result<(), FrameworkError> where S: AsRef, { self.print(stdout(), msg) } /// Print the given message to stderr pub fn print_stderr(self, msg: S) -> Result<(), FrameworkError> where S: AsRef, { self.print(stderr(), msg) } /// Print the given message fn print(self, stream: &StandardStream, msg: S) -> Result<(), FrameworkError> where S: AsRef, { let mut s = stream.lock(); s.reset()?; s.set_color(ColorSpec::new().set_fg(self.color).set_bold(self.bold))?; if let Some(status) = self.status { if self.justified { write!(s, "{:>12}", status)?; } else { write!(s, "{}", status)?; } } s.reset()?; let msg = msg.as_ref(); if !msg.is_empty() { writeln!(s, " {}", msg)?; } s.flush()?; Ok(()) } } abscissa_core-0.8.2/src/terminal/streams.rs000064400000000000000000000007721046102023000170670ustar 00000000000000//! Terminal streams (STDOUT and STDIN) use termcolor::{ColorChoice, StandardStream}; /// Terminal streams pub struct Streams { /// Standard output pub stdout: StandardStream, /// Standard error pub stderr: StandardStream, } impl Streams { /// Create a new set of terminal streams pub fn new(color_choice: ColorChoice) -> Self { Self { stdout: StandardStream::stdout(color_choice), stderr: StandardStream::stderr(color_choice), } } } abscissa_core-0.8.2/src/terminal.rs000064400000000000000000000017161046102023000154100ustar 00000000000000//! Terminal handling (TTY interactions, colors, etc) #[cfg(feature = "application")] pub mod component; #[macro_use] pub mod status; pub mod streams; pub use self::streams::Streams; pub use termcolor::{Color, ColorChoice, StandardStream}; use once_cell::sync::OnceCell; /// Terminal streams static STREAMS: OnceCell = OnceCell::new(); /// Initialize the terminal subsystem, registering the [`Streams`] static pub(crate) fn init(color_choice: ColorChoice) { STREAMS .set(Streams::new(color_choice)) .unwrap_or_else(|_| panic!("terminal streams already initialized!")); } /// Get the terminal [`Streams`]. pub fn streams() -> &'static Streams { STREAMS .get() .expect("terminal streams not yet initialized!") } /// Get the standard output stream pub fn stdout() -> &'static StandardStream { &streams().stdout } /// Get the standard error stream pub fn stderr() -> &'static StandardStream { &streams().stderr } abscissa_core-0.8.2/src/testing/config.rs000064400000000000000000000044321046102023000165150ustar 00000000000000//! Support for writing config files and using them in tests use crate::fs::{self, File, OpenOptions}; use serde::Serialize; use std::{ env, ffi::OsStr, io::{self, Write}, path::{Path, PathBuf}, }; /// Number of times to attempt to create a file before giving up const FILE_CREATE_ATTEMPTS: usize = 1024; /// Configuration file RAII guard which deletes it on completion #[derive(Debug)] pub struct ConfigFile { /// Path to the config file path: PathBuf, } impl ConfigFile { /// Create a config file by serializing it to the given location pub fn create(app_name: &OsStr, config: &C) -> Self where C: Serialize, { let (path, mut file) = Self::open(app_name); let config_toml = toml::to_string_pretty(config) .unwrap_or_else(|e| panic!("error serializing config as TOML: {}", e)) .into_bytes(); file.write_all(&config_toml) .unwrap_or_else(|e| panic!("error writing config to {}: {}", path.display(), e)); Self { path } } /// Get path to the configuration file pub fn path(&self) -> &Path { self.path.as_ref() } /// Create a temporary filename for the config fn open(app_name: &OsStr) -> (PathBuf, File) { // TODO: fully `OsString`-based path building let filename_prefix = app_name.to_string_lossy().to_string(); for n in 0..FILE_CREATE_ATTEMPTS { let filename = format!("{}-{}.toml", &filename_prefix, n); let path = env::temp_dir().join(filename); match OpenOptions::new().write(true).create_new(true).open(&path) { Ok(file) => return (path, file), Err(e) => { if e.kind() == io::ErrorKind::AlreadyExists { continue; } else { panic!("couldn't create {}: {}", path.display(), e); } } } } panic!( "couldn't create {}.toml after {} attempts!", filename_prefix, FILE_CREATE_ATTEMPTS ) } } impl Drop for ConfigFile { fn drop(&mut self) { fs::remove_file(&self.path).unwrap_or_else(|e| { eprintln!("error removing {}: {}", self.path.display(), e); }) } } abscissa_core-0.8.2/src/testing/prelude.rs000064400000000000000000000002241046102023000167030ustar 00000000000000//! Import prelude for Abscissa tests pub use super::{ process::{ExitStatus, OutputStream, Process, Stderr, Stdout}, runner::CmdRunner, }; abscissa_core-0.8.2/src/testing/process/exit_status.rs000064400000000000000000000026741046102023000213100ustar 00000000000000//! Exit statuses for terminated processes use super::Guard; use std::fmt; /// Information about a process's exit status pub struct ExitStatus<'cmd> { /// Exit code code: i32, /// Optional mutex guard ensuring exclusive access to this process. /// /// This is held by the exit status to ensure tests can inspect the results /// of executed commands with the lock held. #[allow(dead_code)] guard: Option>, } impl<'cmd> ExitStatus<'cmd> { /// Create a new exit status pub(super) fn new(code: i32, guard: Option>) -> Self { Self { code, guard } } /// Get the exit code pub fn code(&self) -> i32 { self.code } /// Did the process exit successfully? pub fn success(&self) -> bool { self.code == 0 } /// Assert that the process exited successfully pub fn expect_success(&self) { assert_eq!( 0, self.code, "process exited with error status: {}", self.code ); } /// Assert that the process exited with the given code pub fn expect_code(&self, code: i32) { assert_eq!( code, self.code, "process exited with status code: {} (expected {})", self.code, code ) } } impl<'cmd> fmt::Debug for ExitStatus<'cmd> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "ExitStatus({})", self.code) } } abscissa_core-0.8.2/src/testing/process/streams.rs000064400000000000000000000043621046102023000204060ustar 00000000000000//! Streams (i.e. pipes) for communicating with a subprocess use crate::testing::Regex; use std::{ io::{self, BufRead, BufReader}, ops::{Deref, DerefMut}, process::{ChildStderr, ChildStdout}, }; /// Buffered reader for standard output #[derive(Debug)] pub struct Stdout(BufReader); /// Buffered reader for standard error #[derive(Debug)] pub struct Stderr(BufReader); /// Methods common to output streams pub trait OutputStream: DerefMut> where R: io::Read, { /// Read a line and ensure it matches the expected value. /// /// Panics if it is not the expected value. fn expect_line(&mut self, expected_line: &str) { let mut actual_line = String::new(); self.read_line(&mut actual_line) .unwrap_or_else(|e| panic!("error reading line from {}: {}", stringify!($name), e)); assert_eq!(expected_line, actual_line.trim_end_matches('\n')); } /// Read a line and test it against the given regex. /// /// Panics if the line does not match the regex. fn expect_regex(&mut self, regex: T) where T: Into, { let regex: Regex = regex.into(); let mut line = String::new(); self.read_line(&mut line) .unwrap_or_else(|e| panic!("error reading line from {}: {}", stringify!($name), e)); assert!( regex.is_match(line.trim_end_matches('\n')), "regex {:?} did not match line: {:?}", regex, line ); } } macro_rules! impl_output_stream { ($name:tt, $inner:ty) => { impl $name { /// Create standard output wrapper pub(super) fn new(stream: $inner) -> $name { $name(BufReader::new(stream)) } } impl Deref for $name { type Target = BufReader<$inner>; fn deref(&self) -> &BufReader<$inner> { &self.0 } } impl DerefMut for $name { fn deref_mut(&mut self) -> &mut BufReader<$inner> { &mut self.0 } } impl OutputStream<$inner> for $name {} }; } impl_output_stream!(Stdout, ChildStdout); impl_output_stream!(Stderr, ChildStderr); abscissa_core-0.8.2/src/testing/process.rs000064400000000000000000000056351046102023000167340ustar 00000000000000//! Subprocesses spawned by runners mod exit_status; mod streams; pub use self::{ exit_status::ExitStatus, streams::{OutputStream, Stderr, Stdout}, }; use crate::{ FrameworkError, FrameworkErrorKind::{ProcessError, TimeoutError}, }; use std::{ io::{self, Write}, process::{Child, ChildStdin}, sync::MutexGuard, time::Duration, }; use wait_timeout::ChildExt; /// Mutex guard for a subprocess pub(super) type Guard<'cmd> = MutexGuard<'cmd, ()>; /// Subprocess under test spawned by `CargoRunner` or `CmdRunner` #[derive(Debug)] pub struct Process<'cmd> { /// Child process child: Child, /// Timeout after which process should complete timeout: Duration, /// Standard output (if captured) stdout: Option, /// Standard error (if captured) stderr: Option, /// Standard input stdin: ChildStdin, /// Optional mutex guard ensuring exclusive access to this process guard: Option>, } impl<'cmd> Process<'cmd> { /// Create a process from the given `Child`. /// /// This gets invoked from `CargoRunner::run` pub(super) fn new(mut child: Child, timeout: Duration, guard: Option>) -> Self { let stdout = child.stdout.take().map(Stdout::new); let stderr = child.stderr.take().map(Stderr::new); let stdin = child.stdin.take().unwrap(); Self { child, timeout, stdout, stderr, stdin, guard, } } /// Gets a handle to the child's stdout. /// /// Panics if the child's stdout isn't captured (via `capture_stdout`) pub fn stdout(&mut self) -> &mut Stdout { self.stdout .as_mut() .expect("child stdout not captured (use 'capture_stdout' method)") } /// Gets a handle to the child's stderr. /// /// Panics if the child's stderr isn't captured (via `capture_stderr`) pub fn stderr(&mut self) -> &mut Stderr { self.stderr .as_mut() .expect("child stderr not captured (use 'capture_stderr' method)") } /// Wait for the child to exit pub fn wait(mut self) -> Result, FrameworkError> { match self.child.wait_timeout(self.timeout)? { Some(status) => { let code = status.code().ok_or_else(|| { format_err!(ProcessError, "no exit status returned from subprocess!") })?; Ok(ExitStatus::new(code, self.guard)) } None => fail!( TimeoutError, "operation timed out after {} seconds", self.timeout.as_secs() ), } } } impl<'cmd> Write for Process<'cmd> { fn write(&mut self, buf: &[u8]) -> io::Result { self.stdin.write(buf) } fn flush(&mut self) -> io::Result<()> { self.stdin.flush() } } abscissa_core-0.8.2/src/testing/regex.rs000064400000000000000000000016541046102023000163650ustar 00000000000000//! Regex newtype for simplifying conversions from the `regex` crate use std::{fmt, ops::Deref}; /// Regex newtype (wraps `regex::Regex`) #[derive(Clone)] pub struct Regex(regex::Regex); impl Regex { /// Compile a regular expression pub fn new(re: &str) -> Result { regex::Regex::new(re).map(Regex) } } impl From for Regex { fn from(re: regex::Regex) -> Regex { Regex(re) } } impl From<&str> for Regex { fn from(re: &str) -> Regex { let re_compiled = regex::Regex::new(re) .unwrap_or_else(|err| panic!("error compiling regex: {} ({})", re, err)); Regex(re_compiled) } } impl Deref for Regex { type Target = regex::Regex; fn deref(&self) -> ®ex::Regex { &self.0 } } impl fmt::Debug for Regex { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } abscissa_core-0.8.2/src/testing/runner.rs000064400000000000000000000151071046102023000165620ustar 00000000000000//! Subcommand runners which execute subprocesses use super::{ config::ConfigFile, process::{ExitStatus, Process}, }; use serde::Serialize; use std::{ ffi::OsString, io::{self, Write}, process::{Command, Stdio}, sync::{Arc, Mutex}, time::Duration, }; use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; /// Name of the `cargo` subprocess const CARGO_CMD: &str = "cargo"; /// Length of the default timeout (30 minutes) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30 * 60); /// Run a command via `cargo run` #[derive(Clone, Debug)] pub struct CmdRunner { /// Program to run program: OsString, /// Target binary name target_bin: Option, /// Arguments to pass to the executable args: Vec, /// Capture standard output to a pipe capture_stdout: bool, /// Capture standard error to a pipe capture_stderr: bool, /// Optional configuration file (deleted when no-longer used) config: Option>, /// Print invocation info print_info: bool, /// Optional mutex for serializing usages of this command mutex: Option>>, /// Timeout after which command should complete. timeout: Option, } impl Default for CmdRunner { fn default() -> Self { let mut cmd = Self::new(CARGO_CMD); // Use a `Mutex` to ensure only copy of a command runs at a time cmd.exclusive(); // Add `run` argument to `cargo` cmd.arg("run"); cmd.arg("--"); cmd } } impl CmdRunner { /// Run a target binary via cargo by passing `--bin` to `cargo run`. /// /// Use `CmdRunner::default()` if you only have one target binary in your /// Cargo workspace. pub fn target_bin(bin: S) -> Self where S: Into, { // Start with default settings let mut cmd = Self::default(); // Set memoized target bin let bin = bin.into(); cmd.target_bin = Some(bin.clone()); // Clear arguments and replace them with ones for the given bin cmd.args.clear(); cmd.arg("run"); cmd.arg("--bin"); cmd.arg(bin); cmd.arg("--"); cmd } /// Create a new command runner which runs an arbitrary program at the /// given path. pub fn new(program: S) -> Self where S: Into, { Self { program: program.into(), target_bin: None, args: vec![], capture_stdout: false, capture_stderr: false, config: None, print_info: true, mutex: None, timeout: None, } } /// Append an argument to the set of arguments to run pub fn arg(&mut self, arg: S) -> &mut Self where S: Into, { self.args.push(arg.into()); self } /// Append multiple arguments to the set of arguments to run pub fn args(&mut self, args: I) -> &mut Self where I: IntoIterator, S: Into, { self.args.extend(args.into_iter().map(|a| a.into())); self } /// Enable capturing of standard output pub fn capture_stdout(&mut self) -> &mut Self { self.capture_stdout = true; self } /// Enable capturing of standard error pub fn capture_stderr(&mut self) -> &mut Self { self.capture_stderr = true; self } /// Add the given configuration file pub fn config(&mut self, config: &C) -> &mut Self where C: Serialize, { if self.config.is_some() { panic!("config file already added"); } let target_bin = self .target_bin .as_ref() .cloned() .unwrap_or_else(|| "app".into()); let config_file = ConfigFile::create(&target_bin, config); // Add `abscissa_core::EntryPoint`-compatible args to override config self.arg("-c"); self.arg(config_file.path()); self.config = Some(Arc::new(config_file)); self } /// Serialize invocations of this command using a mutex pub fn exclusive(&mut self) -> &mut Self { if self.mutex.is_none() { self.mutex = Some(Arc::new(Mutex::new(()))) } self } /// Disable printing a `+ run: ...` logline when running command pub fn quiet(&mut self) -> &mut Self { self.print_info = false; self } /// Set the timeout after which the command should complete. /// /// By default `CargoRunner` timeout will be used (30 minutes). pub fn timeout(&mut self, duration: Duration) -> &mut Self { self.timeout = Some(duration); self } /// Run the given subcommand pub fn run(&self) -> Process<'_> { let guard = self .mutex .as_ref() .map(|mutex| mutex.lock().expect("poisoned cmd mutex!")); if self.print_info { self.print_command().unwrap(); } let stdout = if self.capture_stdout { Stdio::piped() } else { Stdio::inherit() }; let stderr = if self.capture_stderr { Stdio::piped() } else { Stdio::inherit() }; let child = Command::new(&self.program) .args(&self.args) .stdin(Stdio::piped()) .stdout(stdout) .stderr(stderr) .spawn() .unwrap_or_else(|e| { panic!("error running command: {}", e); }); Process::new(child, self.timeout.unwrap_or(DEFAULT_TIMEOUT), guard) } /// Get the exit status for the given subcommand pub fn status(&self) -> ExitStatus<'_> { self.run().wait().unwrap_or_else(|e| { panic!("error waiting for subprocess to terminate: {}", e); }) } /// Print the command we're about to run fn print_command(&self) -> Result<(), io::Error> { let stdout = BufferWriter::stdout(ColorChoice::Auto); let mut buffer = stdout.buffer(); buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?; write!(&mut buffer, "+ ")?; buffer.set_color(ColorSpec::new().set_fg(Some(Color::White)).set_bold(true))?; write!(&mut buffer, "run")?; buffer.reset()?; let cmd = self.program.to_string_lossy(); let args: Vec<_> = self.args.iter().map(|arg| arg.to_string_lossy()).collect(); writeln!(&mut buffer, ": {} {}", cmd, args.join(" "))?; stdout.print(&buffer) } } abscissa_core-0.8.2/src/testing.rs000064400000000000000000000005461046102023000152520ustar 00000000000000//! Acceptance testing for Abscissa applications. //! //! The recommended way to import types for testing is: //! //! ``` //! use abscissa_core::testing::prelude::*; //! ``` //! //! The main entrypoint for running tests is [`CmdRunner`]. mod config; pub mod prelude; pub mod process; mod regex; mod runner; pub use self::{regex::Regex, runner::CmdRunner}; abscissa_core-0.8.2/src/thread/kill_switch.rs000064400000000000000000000027721046102023000173630ustar 00000000000000//! Kill switches are a cooperative approach to requesting a thread terminate, //! by setting a flag in one thread that another thread can periodically check //! in order to determine if it should exit. thread_local! { /// Boolean flag signaling to a thread to terminate static KILL_SWITCH: RefCell>> = RefCell::new(None); } use std::{ cell::RefCell, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, }; /// Thread kill switch. /// /// This is a signal that the thread should terminate. #[derive(Debug)] pub(super) struct KillSwitch(AtomicBool); impl KillSwitch { /// Create a new kill switch pub fn new() -> KillSwitch { KillSwitch(AtomicBool::new(false)) } /// Throw the kill switch, indicating it's time to terminate pub fn throw(&self) { self.0.store(true, Ordering::Relaxed); } /// Has the kill switch been thrown? pub fn is_thrown(&self) -> bool { self.0.load(Ordering::Relaxed) } } /// Check whether the kill switch for this thread has been thrown. /// /// Panics if no kill switch is configured for the current thread. pub(super) fn is_thrown() -> bool { KILL_SWITCH.with(|ks| { ks.borrow() .as_ref() .expect("no kill switch configured for current thread") .is_thrown() }) } /// Set the kill switch value for the current thread pub(super) fn set(kill_switch: Arc) { KILL_SWITCH.with(|ks| *ks.borrow_mut() = Some(kill_switch)); } abscissa_core-0.8.2/src/thread/manager.rs000064400000000000000000000036111046102023000164520ustar 00000000000000//! Thread manager. use super::{Name, Thread}; use crate::{FrameworkError, FrameworkErrorKind::ThreadError, Map}; use std::{convert::TryInto, sync}; /// Reader guard for the thread manager. pub type Reader<'a> = sync::RwLockReadGuard<'a, Manager>; /// Writer guard for the thread manager. pub type Writer<'a> = sync::RwLockWriteGuard<'a, Manager>; /// Thread manager that tracks threads spawned by the application and handles /// shutting them down. #[derive(Debug, Default)] pub struct Manager { threads: Map, } impl Manager { /// Spawn a thread within the thread manager. pub fn spawn(&mut self, name: impl TryInto, f: F) -> Result<(), FrameworkError> where F: FnOnce() + Send + 'static, { // TODO(tarcieri): propagate underlying error (after error handling refactor) let name = name .try_into() .ok() .ok_or_else(|| format_err!(ThreadError, "invalid thread name"))?; if self.threads.contains_key(&name) { fail!(ThreadError, "duplicate name: {}", name); } let thread = Thread::spawn(name.clone(), f)?; self.threads.insert(name, thread); Ok(()) } /// Signal all running threads to terminate and then join them pub fn join(&mut self) -> Result<(), FrameworkError> { let mut names = Vec::with_capacity(self.threads.len()); // Send termination request in advance prior to joining for (name, thread) in self.threads.iter() { names.push(name.clone()); thread.request_termination(); } // TODO(tarcieri): use `BTreeMap::into_values` when stable // See: for name in names.into_iter() { if let Some(thread) = self.threads.remove(&name) { thread.join()?; } } Ok(()) } } abscissa_core-0.8.2/src/thread/name.rs000064400000000000000000000017751046102023000157710ustar 00000000000000//! Thread names. use crate::{FrameworkError, FrameworkErrorKind::ThreadError}; use std::{fmt, str::FromStr}; /// Thread name. /// /// Cannot contain null bytes. #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct Name(String); impl Name { /// Create a new thread name pub fn new(name: impl ToString) -> Result { let name = name.to_string(); if name.contains('\0') { fail!(ThreadError, "name contains null bytes: {:?}", name) } else { Ok(Name(name)) } } } impl AsRef for Name { fn as_ref(&self) -> &str { &self.0 } } impl FromStr for Name { type Err = FrameworkError; fn from_str(s: &str) -> Result { Self::new(s) } } impl From for String { fn from(name: Name) -> String { name.0 } } impl fmt::Display for Name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } abscissa_core-0.8.2/src/thread.rs000064400000000000000000000052441046102023000150440ustar 00000000000000//! Thread wrapper types. //! //! These types provide simple wrappers for Rust's core threading primitives. mod kill_switch; pub mod manager; mod name; pub use self::{manager::Manager, name::Name}; use self::kill_switch::KillSwitch; use crate::{FrameworkError, FrameworkErrorKind::ThreadError}; use std::{io, sync::Arc, thread}; /// Join handles for Abscissa-managed threads. pub type JoinHandle = thread::JoinHandle<()>; /// Threads spawned and managed by Abscissa #[derive(Debug)] pub struct Thread { /// Name of the current thread name: Name, /// Kill switch used to terminate the thread kill_switch: Arc, /// Join handle to the thread handle: JoinHandle, } impl Thread { /// Spawn a new thread, executing the given runnable pub fn spawn(name: Name, f: F) -> Result where F: FnOnce() + Send + 'static, { let kill_switch = Arc::new(KillSwitch::new()); let handle = spawn_thread(name.clone(), Arc::clone(&kill_switch), f)?; Ok(Self { name, kill_switch, handle, }) } /// Get the name of this thread. pub fn name(&self) -> &Name { &self.name } /// Request that this thread terminate. /// /// Note this does not have immediate effect: it signals to the thread /// that it should exit, however the target thread needs to poll the /// `Thread::should_terminate()` flag in order to receive this signal /// (and exit accordingly when it is set). pub fn request_termination(&self) { self.kill_switch.throw(); } /// Join to a running thread, waiting for it to finish pub fn join(self) -> Result<(), FrameworkError> { // Trigger the kill switch in order to signal the thread to stop. self.request_termination(); // Wait for the other thread to exit self.handle .join() .map_err(|e| format_err!(ThreadError, "{:?}", e))?; Ok(()) } } /// Check whether the currently running thread should exit, as signaled by /// `Thread::request_termination()`. /// /// Panics if called outside a thread spawned by `abscissa_core::Thread`. pub fn should_terminate() -> bool { kill_switch::is_thrown() } /// Spawn a thread fn spawn_thread(name: Name, kill_switch: Arc, f: F) -> Result where F: FnOnce() + Send + 'static, { // NOTE: `Name` ensures the absence of null bytes, which should prevent the // only condition under which this function could potentially panic. thread::Builder::new().name(name.into()).spawn(move || { kill_switch::set(kill_switch); f() }) } abscissa_core-0.8.2/src/trace/component.rs000064400000000000000000000044561046102023000167010ustar 00000000000000//! Abscissa tracing component // TODO(tarcieri): logfile support? use tracing_log::LogTracer; use tracing_subscriber::{fmt::Formatter, reload::Handle, EnvFilter, FmtSubscriber}; use super::config::Config; use crate::{terminal::ColorChoice, Component, FrameworkError, FrameworkErrorKind}; /// Abscissa component for initializing the `tracing` subsystem #[derive(Component, Debug)] #[component(core)] pub struct Tracing { filter_handle: Handle, } impl Tracing { /// Create a new [`Tracing`] component from the given [`Config`]. pub fn new(config: Config, color_choice: ColorChoice) -> Result { // Configure log/tracing interoperability by setting a `LogTracer` as // the global logger for the log crate, which converts all log events // into tracing events. LogTracer::init().map_err(|e| FrameworkErrorKind::ComponentError.context(e))?; // Construct a tracing subscriber with the supplied filter and enable reloading. let builder = FmtSubscriber::builder() .with_ansi(match color_choice { ColorChoice::Always => true, ColorChoice::AlwaysAnsi => true, ColorChoice::Auto => true, ColorChoice::Never => false, }) .with_env_filter(config.filter) .with_filter_reloading(); let filter_handle = builder.reload_handle(); let subscriber = builder.finish(); // Now set it as the global tracing subscriber and save the handle. tracing::subscriber::set_global_default(subscriber) .map_err(|e| FrameworkErrorKind::ComponentError.context(e))?; Ok(Self { filter_handle }) } /// Return the currently-active tracing filter. pub fn filter(&self) -> String { self.filter_handle .with_current(|filter| filter.to_string()) .expect("the subscriber is not dropped before the component is") } /// Reload the currently-active filter with the supplied value. /// /// This can be used to provide a dynamic tracing filter endpoint. pub fn reload_filter(&mut self, filter: impl Into) { self.filter_handle .reload(filter) .expect("the subscriber is not dropped before the component is"); } } abscissa_core-0.8.2/src/trace/config.rs000064400000000000000000000010311046102023000161260ustar 00000000000000//! Logging configuration /// Tracing configuration #[derive(Clone, Debug, Eq, PartialEq)] pub struct Config { pub(super) filter: String, } impl Config { /// Create a config for verbose output. pub fn verbose() -> Self { "debug".to_owned().into() } } impl Default for Config { fn default() -> Self { std::env::var("RUST_LOG") .unwrap_or("info".to_owned()) .into() } } impl From for Config { fn from(filter: String) -> Self { Self { filter } } } abscissa_core-0.8.2/src/trace.rs000064400000000000000000000002671046102023000146730ustar 00000000000000//! Tracing subsystem #[cfg(feature = "application")] pub mod component; mod config; #[cfg(feature = "application")] pub use self::component::Tracing; pub use self::config::Config; abscissa_core-0.8.2/tests/component.rs000064400000000000000000000117211046102023000161470ustar 00000000000000//! Tests for Abscissa's component functionality mod example_app; use self::example_app::{ExampleApp, ExampleConfig}; use abscissa_core::{component, Component, FrameworkError, FrameworkErrorKind::ComponentError}; /// ID for `FoobarComponent` (example component #1) const FOOBAR_COMPONENT_ID: component::Id = component::Id::new("component::FoobarComponent"); /// ID for `BazComponent` (example component #2) const BAZ_COMPONENT_ID: component::Id = component::Id::new("component::BazComponent"); /// ID for `QuuxComponent` (example component #3) const QUUX_COMPONENT_ID: component::Id = component::Id::new("component::QuuxComponent"); /// Example component #1 #[derive(Component, Debug, Default)] pub struct FoobarComponent { /// Component state pub state: Option, } impl FoobarComponent { /// Set the state string to a particular value pub fn set_state(&mut self, state_str: &str) { self.state = Some(state_str.to_owned()); } } /// Example component #2 #[derive(Component, Debug, Default)] pub struct BazComponent {} /// Example component #3 #[derive(Component, Debug, Default)] #[component(inject = "init_foobar(component::FoobarComponent)")] #[component(inject = "init_baz(component::BazComponent)")] pub struct QuuxComponent { /// State of Foo at the time we were initialized? pub foobar_state: Option, /// Did we get a callback that `Baz` has been initialized? pub baz_initialized: bool, } impl QuuxComponent { /// Callback run after `FoobarComponent` has been initialized pub fn init_foobar(&mut self, foobar: &mut FoobarComponent) -> Result<(), FrameworkError> { self.foobar_state = foobar.state.clone(); foobar.state = Some("hijacked!".to_owned()); Ok(()) } /// Callback run after `BazComponent` has been initialized pub fn init_baz(&mut self, _baz: &BazComponent) -> Result<(), FrameworkError> { self.baz_initialized = true; Ok(()) } } fn init_components() -> Vec>> { let mut foobar = FoobarComponent::default(); foobar.set_state("original foobar state"); let component1 = Box::new(foobar); let component2 = Box::::default(); let component3 = Box::::default(); let components: Vec>> = vec![component1, component2, component3]; // Ensure component IDs are as expected assert_eq!(components[0].id(), FOOBAR_COMPONENT_ID); assert_eq!(components[1].id(), BAZ_COMPONENT_ID); assert_eq!(components[2].id(), QUUX_COMPONENT_ID); components } #[test] fn component_registration() { let mut registry = component::Registry::default(); assert!(registry.is_empty()); let components = init_components(); registry.register(components).unwrap(); assert_eq!(registry.len(), 3); // Fetch components and make sure they got registered correctly let foobar_comp = registry.get_by_id(FOOBAR_COMPONENT_ID).unwrap(); assert_eq!(foobar_comp.id(), FOOBAR_COMPONENT_ID); let baz_comp = registry.get_by_id(BAZ_COMPONENT_ID).unwrap(); assert_eq!(baz_comp.id(), BAZ_COMPONENT_ID); let quux_comp = registry.get_by_id(QUUX_COMPONENT_ID).unwrap(); assert_eq!(quux_comp.id(), QUUX_COMPONENT_ID); } #[test] fn duplicate_component_registration() { let foobar1 = Box::::default(); let foobar2 = Box::::default(); let components: Vec>> = vec![foobar1, foobar2]; let mut registry = component::Registry::default(); assert!(registry.is_empty()); let err = registry.register(components).err().unwrap(); assert_eq!(*err.kind(), ComponentError); assert_eq!(registry.len(), 1); let foobar = registry.get_by_id(FOOBAR_COMPONENT_ID).unwrap(); assert_eq!(foobar.id(), FOOBAR_COMPONENT_ID); } #[test] fn get_downcast_ref() { let mut registry = component::Registry::default(); let component = Box::::default() as Box>; registry.register(vec![component]).unwrap(); { let foo_mut = registry.get_downcast_mut::().unwrap(); foo_mut.set_state("mutated!"); } { let foo_comp = registry.get_downcast_ref::().unwrap(); assert_eq!(foo_comp.state.as_ref().unwrap(), "mutated!"); } } #[test] fn dependency_injection() { let mut registry = component::Registry::default(); let mut components = init_components(); // Start component up in reverse order to make sure sorting works components.reverse(); registry.register(components).unwrap(); registry.after_config(&ExampleConfig::default()).unwrap(); let foobar_comp = registry.get_downcast_ref::().unwrap(); assert_eq!(foobar_comp.state.as_ref().unwrap(), "hijacked!"); let quux_comp = registry.get_downcast_ref::().unwrap(); assert_eq!( quux_comp.foobar_state.as_ref().unwrap(), "original foobar state" ); } abscissa_core-0.8.2/tests/example_app/mod.rs000064400000000000000000000030461046102023000172200ustar 00000000000000//! Example application used for testing purposes use abscissa_core::{ application, clap::Parser, config, Application, Command, Configurable, FrameworkError, Runnable, StandardPaths, }; use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct ExampleConfig {} #[derive(Command, Debug, Parser)] pub struct ExampleCommand {} impl Configurable for ExampleCommand { fn config_path(&self) -> Option { None } } impl Runnable for ExampleCommand { fn run(&self) { unimplemented!(); } } #[derive(Debug, Default)] pub struct ExampleApp { config: Option, state: application::State, } impl Application for ExampleApp { type Cmd = ExampleCommand; type Cfg = ExampleConfig; type Paths = StandardPaths; fn config(&self) -> config::Reader { unimplemented!(); } fn state(&self) -> &application::State { unimplemented!(); } fn register_components(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> { let framework_components = self.framework_components(command)?; let mut app_components = self.state.components_mut(); app_components.register(framework_components) } fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> { let mut components = self.state.components_mut(); components.after_config(&config)?; self.config = Some(config); Ok(()) } }