diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddc2112..7089015 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: pull_request: push: - branches: ["*"] + branches: ["master"] jobs: build: diff --git a/Cargo.lock b/Cargo.lock index 0d3f2de..3fce6f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,6 +16,11 @@ dependencies = [ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "anyhow" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "arrayref" version = "0.3.5" @@ -339,6 +344,11 @@ name = "fnv" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "fs-err" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -403,6 +413,14 @@ dependencies = [ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "http" version = "0.1.18" @@ -669,6 +687,30 @@ name = "ppv-lite86" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "proc-macro-error" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro-error-attr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "proc-macro2" version = "0.4.30" @@ -758,13 +800,13 @@ dependencies = [ [[package]] name = "rand" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "getrandom 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -783,7 +825,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -801,7 +843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "rand_core" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "getrandom 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -820,7 +862,7 @@ name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -982,19 +1024,23 @@ dependencies = [ name = "run-in-roblox" version = "0.2.0" dependencies = [ - "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)", "colored 1.9.3 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fs-err 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rbx_binary 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "rbx_dom_weak 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "rbx_xml 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", "roblox_install 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1157,6 +1203,28 @@ name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "structopt" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt-derive 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "structopt-derive" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-error 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "syn" version = "0.15.43" @@ -1177,6 +1245,16 @@ dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "synstructure" version = "0.10.2" @@ -1204,7 +1282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1226,6 +1304,24 @@ dependencies = [ "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "thiserror" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "thiserror-impl 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "thread_local" version = "0.3.6" @@ -1371,6 +1467,11 @@ name = "try-lock" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicode-width" version = "0.1.5" @@ -1391,7 +1492,7 @@ name = "uuid" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1405,6 +1506,11 @@ name = "version_check" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "walkdir" version = "2.2.9" @@ -1496,6 +1602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] "checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum anyhow 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)" = "d9a60d744a80c30fcb657dfe2c1b22bcb3e814c1a1e3674f32bf5820b570fbff" "checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" "checksum arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba" "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" @@ -1534,6 +1641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" +"checksum fs-err 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c1a51f8b7158efbe531f7baa74e38e49fbc41239e5d66720bb37ed39c27c241a" "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" @@ -1542,6 +1650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum getrandom 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "34f33de6f0ae7c9cb5e574502a562e2b512799e32abb801cd1e79ad952b62b49" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" "checksum h2 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" +"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum http 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "372bcb56f939e449117fb0869c2e8fd8753a8223d92a172c6e808cf123a5b6e4" "checksum http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" "checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" @@ -1572,6 +1681,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum parking_lot_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad7f7e6ebdc79edff6fdcb87a55b620174f7a989e3eb31b65231f4af57f00b8c" "checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" "checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b" +"checksum proc-macro-error 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" +"checksum proc-macro-error-attr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" "checksum pulldown-cmark 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eef52fac62d0ea7b9b4dc7da092aa64ea7ec3d90af6679422d3d7e0e14b6ee15" @@ -1581,12 +1692,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" "checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" "checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -"checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c" +"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" "checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" "checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" "checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" -"checksum rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "615e683324e75af5d43d8f7a39ffe3ee4a9dc42c5c701167a71dc59c3a493aca" +"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" "checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" "checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" @@ -1626,13 +1737,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" "checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +"checksum structopt 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "ff6da2e8d107dfd7b74df5ef4d205c6aebee0706c647f6bc6a2d5789905c00fb" +"checksum structopt-derive 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a489c87c08fbaf12e386665109dd13470dcc9c4583ea3e10dd2b4523e5ebd9ac" "checksum syn 0.15.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ee06ea4b620ab59a2267c6b48be16244a3389f8bfa0986bdd15c35b890b00af3" "checksum syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" +"checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" "checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" "checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +"checksum thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "f0570dc61221295909abdb95c739f2e74325e14293b2026b0a7e195091ec54ae" +"checksum thiserror-impl 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "227362df41d566be41a28f64401e07a043157c21c14b9785a0d8e256f940a8fd" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" "checksum tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" @@ -1646,12 +1762,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum tokio-threadpool 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "90ca01319dea1e376a001e8dc192d42ebde6dd532532a5bad988ac37db365b19" "checksum tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "f2106812d500ed25a4f38235b9cae8f78a09edf43203e16e59c3b769a342a60e" "checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" +"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" "checksum walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9658c94fa8b940eab2250bd5a457f9c48b748420d71293b165c8cdbe2f55f71e" "checksum want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" diff --git a/Cargo.toml b/Cargo.toml index 1ed8699..8243bce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,25 +8,25 @@ repository = "https://github.com/rojo-rbx/run-in-roblox" license = "MIT" readme = "README.md" -[lib] -name = "run_in_roblox" -path = "src/lib.rs" - [[bin]] name = "run-in-roblox" path = "src/main.rs" [dependencies] -clap = "2.33.0" +anyhow = "1.0.28" colored = "1.9.3" env_logger = "0.7.1" +fs-err = "2.3.0" futures = "0.1.25" hyper = "0.12.35" log = "0.4.8" +rand = "0.7.3" rbx_binary = "0.5.0" rbx_dom_weak = "1.10.1" rbx_xml = "0.11.4" roblox_install = "0.2.2" serde = { version = "1.0.106", features = ["derive"] } serde_json = "1.0.51" +structopt = "0.3.13" tempfile = "3.1.0" +thiserror = "1.0.14" diff --git a/demo/default.project.json b/demo/default.project.json index 3f2d431..5006e1e 100644 --- a/demo/default.project.json +++ b/demo/default.project.json @@ -1,21 +1,6 @@ { - "name": "run-in-roblox demo", - "tree": { - "$className": "DataModel", - "ReplicatedStorage": { - "$className": "ReplicatedStorage", - "src": { - "$path": "src" - } - }, - "HttpService": { - "$className": "HttpService", - "$properties": { - "HttpEnabled": { - "Type": "Bool", - "Value": true - } - } - } - } + "name": "run-in-roblox demo", + "tree": { + "$className": "DataModel" + } } \ No newline at end of file diff --git a/demo/hello.lua b/demo/hello.lua new file mode 100644 index 0000000..31b6892 --- /dev/null +++ b/demo/hello.lua @@ -0,0 +1,2 @@ +print("Hello, world!") +error("nah") \ No newline at end of file diff --git a/demo/src/main.server.lua b/demo/src/main.server.lua deleted file mode 100644 index 1385fe3..0000000 --- a/demo/src/main.server.lua +++ /dev/null @@ -1 +0,0 @@ -print("Hello, world!") \ No newline at end of file diff --git a/src/core.rs b/src/core.rs deleted file mode 100644 index b2598e9..0000000 --- a/src/core.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::{ - collections::HashMap, - path::Path, - sync::mpsc::{self, Receiver}, -}; - -use log::info; -use rbx_dom_weak::{RbxInstanceProperties, RbxTree}; - -use crate::{ - message_receiver::RobloxMessage, - place_runner::{open_rbx_place_file, PlaceRunner, PlaceRunnerOptions}, -}; - -pub const DEFAULT_PORT: u16 = 54023; -pub const DEFAULT_TIMEOUT: u16 = 15; - -pub fn run_place( - path: &Path, - extension: &str, - options: PlaceRunnerOptions, -) -> Receiver> { - let tree = open_rbx_place_file(path, extension); - - let place = PlaceRunner::new(tree, options); - let (message_tx, message_rx) = mpsc::channel(); - - place.run_with_sender(message_tx); - - message_rx -} - -pub fn run_model(_path: &Path, _extension: &str) -> Receiver> { - unimplemented!("Models are not yet supported by run-in-roblox."); -} - -pub fn run_script(options: PlaceRunnerOptions) -> Receiver> { - let tree = RbxTree::new(RbxInstanceProperties { - name: String::from("Place"), - class_name: String::from("DataModel"), - properties: HashMap::new(), - }); - - let place = PlaceRunner::new(tree, options); - let (message_tx, message_rx) = mpsc::channel(); - - info!("Running place..."); - place.run_with_sender(message_tx); - - message_rx -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 6abbf74..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,74 +0,0 @@ -mod core; -mod message_receiver; -mod place; -mod place_runner; -mod plugin; - -use std::{ - error::Error, - fmt, - path::{Path, PathBuf}, -}; - -use crate::{ - core::{run_place, run_script, DEFAULT_PORT, DEFAULT_TIMEOUT}, - message_receiver::RobloxMessage, - place_runner::PlaceRunnerOptions, -}; - -#[derive(Debug)] -struct BadPathError { - path: PathBuf, -} - -impl Error for BadPathError {} - -impl fmt::Display for BadPathError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Invalid file type: {}", self.path.display()) - } -} - -pub enum RunEnvironment<'a> { - EmptyPlace, - PlaceFile(&'a Path), -} - -pub struct RunOptions<'a> { - pub env: RunEnvironment<'a>, - pub lua_script: &'a str, - pub port: Option, - pub timeout: Option, -} - -pub fn run(opts: RunOptions) -> Result, Box> { - let place_runner_opts = PlaceRunnerOptions { - lua_script: opts.lua_script, - port: opts.port.unwrap_or(DEFAULT_PORT), - timeout: opts.timeout.unwrap_or(DEFAULT_TIMEOUT), - }; - - let message_queue = match opts.env { - RunEnvironment::EmptyPlace => run_script(place_runner_opts), - RunEnvironment::PlaceFile(path) => { - let extension = match path.extension() { - Some(e) => e.to_str().unwrap(), - None => { - return Err(Box::new(BadPathError { - path: path.to_path_buf(), - })) - } - }; - - run_place(path, extension, place_runner_opts) - } - }; - - let mut messages = Vec::new(); - - while let Some(message) = message_queue.recv()? { - messages.push(message); - } - - Ok(messages) -} diff --git a/src/main.rs b/src/main.rs index 58b11c0..fb76cd3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,44 +1,107 @@ -mod core; mod message_receiver; -mod place; mod place_runner; mod plugin; -use std::{fs::read_to_string, path::Path, process}; +use std::{path::PathBuf, process, sync::mpsc, thread}; -use clap::{App, Arg}; +use anyhow::anyhow; use colored::Colorize; -use log::error; +use fs_err as fs; +use structopt::StructOpt; +use tempfile::tempdir; use crate::{ - core::{run_model, run_place, run_script, DEFAULT_PORT, DEFAULT_TIMEOUT}, message_receiver::{OutputLevel, RobloxMessage}, - place_runner::PlaceRunnerOptions, + place_runner::PlaceRunner, }; -fn print_message(message: &RobloxMessage) { - match message { - RobloxMessage::Output { level, body } => { - println!( - "{}", - match level { +#[derive(Debug, StructOpt)] +struct Options { + /// A path to the place file to open in Roblox Studio. If not specified, an + /// empty place file is used. + #[structopt(long("place"))] + place_path: Option, + + /// A path to the script to run in Roblox Studio. + /// + /// The script will be run at plugin-level security. + #[structopt(long("script"))] + script_path: PathBuf, +} + +fn run(options: Options) -> Result { + // Create a temp directory to house our place, even if a path is given from + // the command line. This helps ensure Studio won't hang trying to tell the + // user that the place is read-only because of a .lock file. + let temp_place_folder = tempdir()?; + let temp_place_path; + + match &options.place_path { + Some(place_path) => { + let extension = place_path + .extension() + .ok_or_else(|| anyhow!("Place file did not have a file extension"))? + .to_str() + .ok_or_else(|| anyhow!("Place file extension had invalid Unicode"))?; + + temp_place_path = temp_place_folder + .path() + .join(format!("run-in-roblox-place.{}", extension)); + + fs::copy(place_path, &temp_place_path)?; + } + None => { + unimplemented!("run-in-roblox with no place argument"); + } + } + + let script_contents = fs::read_to_string(&options.script_path)?; + + // Generate a random, unique ID for this session. The plugin we inject will + // compare this value with the one reported by the server and abort if they + // don't match. + let server_id = format!("run-in-roblox-{:x}", rand::random::()); + + let place_runner = PlaceRunner { + port: 50312, + place_path: temp_place_path.clone(), + server_id: server_id.clone(), + lua_script: script_contents.clone(), + }; + + let (sender, receiver) = mpsc::channel(); + + thread::spawn(move || { + place_runner.run(sender).unwrap(); + }); + + let mut exit_code = 0; + + while let Some(message) = receiver.recv()? { + match message { + RobloxMessage::Output { level, body } => { + let colored_body = match level { OutputLevel::Print => body.normal(), OutputLevel::Info => body.cyan(), OutputLevel::Warning => body.yellow(), OutputLevel::Error => body.red(), + }; + + println!("{}", colored_body); + + if level == OutputLevel::Error { + exit_code = 1; } - ); + } } } -} -fn bad_path(path: &Path) -> ! { - error!("Type of path {} could not be detected.", path.display()); - error!("Valid extensions are .lua, .rbxm, .rbxmx, .rbxl, and .rbxlx."); - process::exit(1); + Ok(exit_code) } fn main() { + let options = Options::from_args(); + { let log_env = env_logger::Env::default().default_filter_or("warn"); @@ -47,128 +110,11 @@ fn main() { .init(); } - let default_port = DEFAULT_PORT.to_string(); - let default_timeout = DEFAULT_TIMEOUT.to_string(); - - let app = App::new("run-in-roblox") - .author(env!("CARGO_PKG_AUTHORS")) - .version(env!("CARGO_PKG_VERSION")) - .about(env!("CARGO_PKG_DESCRIPTION")) - .arg( - Arg::with_name("PATH") - .help("The place, model, or script file to run inside Roblox") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name("script") - .short("s") - .help("The lua script file to be executed in the opened place") - .takes_value(true) - .required(false) - .conflicts_with("execute"), - ) - .arg( - Arg::with_name("execute") - .short("e") - .help("The lua string to execute") - .takes_value(true) - .required(false) - .conflicts_with("script"), - ) - .arg( - Arg::with_name("port") - .short("p") - .help("The port used by the local server") - .takes_value(true) - .default_value(&default_port), - ) - .arg( - Arg::with_name("timeout") - .short("t") - .help("The maximum time in seconds that the script can run") - .takes_value(true) - .default_value(&default_timeout), - ); - - let matches = app.get_matches(); - - let input = Path::new(matches.value_of("PATH").unwrap()); - - let port = match matches.value_of("port") { - Some(port) => port - .parse::() - .expect("port must be an unsigned integer"), - None => DEFAULT_PORT, - }; - - let timeout = match matches.value_of("timeout") { - Some(timeout) => timeout - .parse::() - .expect("timeout must be an unsigned integer"), - None => DEFAULT_TIMEOUT, - }; - - let extension = match input.extension() { - Some(e) => e.to_str().unwrap(), - None => bad_path(input), - }; - - let messages = match extension { - "lua" => { - if let Some(_) = matches.value_of("script") { - panic!("Cannot provide script argument when running a script file (remove `--script LUA_FILE_PATH`)") - }; - if let Some(_) = matches.value_of("execute") { - panic!("Cannot provide execute argument when running a script file (remove `--execute LUA_CODE`)") - }; - - let lua_script = read_to_string(input).expect("Something went wrong reading the file"); - - run_script(PlaceRunnerOptions { - port, - timeout, - lua_script: lua_script.as_ref(), - }) + match run(options) { + Ok(exit_code) => process::exit(exit_code), + Err(err) => { + log::error!("{:?}", err); + process::exit(2); } - "rbxm" | "rbxmx" => run_model(input, extension), - "rbxl" | "rbxlx" => { - let lua_script = if let Some(script_file_path) = matches.value_of("script") { - read_to_string(script_file_path).expect("Something went wrong reading the file") - } else if let Some(lua_string) = matches.value_of("execute") { - lua_string.to_owned() - } else { - panic!( - "Lua code not provided (use `--script LUA_FILE_PATH` or `--execute LUA_CODE`)" - ) - }; - - run_place( - input, - extension, - PlaceRunnerOptions { - port, - timeout, - lua_script: lua_script.as_ref(), - }, - ) - } - _ => bad_path(input), - }; - - let mut exit_code = 0; - - while let Some(message) = messages.recv().expect("Problem receiving message") { - if let RobloxMessage::Output { - level: OutputLevel::Error, - .. - } = message - { - exit_code = 1; - } - - print_message(&message); } - - process::exit(exit_code); } diff --git a/src/message_receiver.rs b/src/message_receiver.rs index cc62abd..716b884 100644 --- a/src/message_receiver.rs +++ b/src/message_receiver.rs @@ -1,4 +1,8 @@ -use std::{fmt, sync::mpsc, thread, time::Duration}; +use std::{ + sync::{mpsc, Arc}, + thread, + time::Duration, +}; use futures::{future, stream::Stream, sync::oneshot, Future}; use hyper::{service::service_fn, Body, Method, Request, Response, Server, StatusCode}; @@ -19,39 +23,18 @@ pub enum RobloxMessage { Output { level: OutputLevel, body: String }, } -impl fmt::Display for RobloxMessage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RobloxMessage::Output { body, level } => match level { - OutputLevel::Print => write!(f, "{}", body), - _ => write!(f, "[{}]: {}", level, body), - }, - } - } -} - -#[derive(Debug, Clone, Copy, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] pub enum OutputLevel { - Info, Print, + Info, Warning, Error, } -impl fmt::Display for OutputLevel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - OutputLevel::Print => write!(f, "PRINT"), - OutputLevel::Info => write!(f, "INFO"), - OutputLevel::Warning => write!(f, "WARN"), - OutputLevel::Error => write!(f, "ERROR"), - } - } -} - #[derive(Debug)] pub struct MessageReceiverOptions { pub port: u16, + pub server_id: String, } pub struct MessageReceiver { @@ -64,17 +47,23 @@ impl MessageReceiver { let (message_tx, message_rx) = mpsc::channel(); let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); + let server_id = Arc::new(options.server_id.clone()); + thread::spawn(move || { let service = move || { + let server_id = server_id.clone(); let message_tx = message_tx.clone(); service_fn(move |request: Request| -> HyperResponse { + let server_id = server_id.clone(); let message_tx = message_tx.clone(); let mut response = Response::new(Body::empty()); + log::debug!("Request: {} {}", request.method(), request.uri().path()); + match (request.method(), request.uri().path()) { (&Method::GET, "/") => { - *response.body_mut() = Body::from("Hey there!"); + *response.body_mut() = Body::from(server_id.as_str().to_owned()); } (&Method::POST, "/start") => { message_tx.send(Message::Start).unwrap(); diff --git a/src/place.rs b/src/place.rs deleted file mode 100644 index d2974ee..0000000 --- a/src/place.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::{collections::HashMap, io::Write}; - -use rbx_dom_weak::{RbxInstanceProperties, RbxTree, RbxValue}; -use rbx_xml::EncodeError; - -pub struct RunInRbxPlace { - tree: RbxTree, -} - -impl RunInRbxPlace { - pub fn new(mut tree: RbxTree, port: u16) -> RunInRbxPlace { - enable_http(&mut tree); - add_plugin_marker(&mut tree, port); - - RunInRbxPlace { tree } - } - - pub fn write(&self, output: W) -> Result<(), EncodeError> { - let root_id = self.tree.get_root_id(); - let top_level_ids = self.tree.get_instance(root_id).unwrap().get_children_ids(); - - rbx_xml::to_writer_default(output, &self.tree, top_level_ids) - } -} - -fn add_plugin_marker(tree: &mut RbxTree, port: u16) { - let mut properties = HashMap::new(); - properties.insert( - String::from("Value"), - RbxValue::Int32 { value: port as i32 }, - ); - - let marker = RbxInstanceProperties { - name: String::from("RUN_IN_ROBLOX_PORT"), - class_name: String::from("IntValue"), - properties, - }; - - let root_id = tree.get_root_id(); - tree.insert_instance(marker, root_id); -} - -fn enable_http(tree: &mut RbxTree) { - let http_service_id = match tree - .descendants(tree.get_root_id()) - .find(|descendant| descendant.class_name == "HttpService") - { - Some(http_service) => Some(http_service.get_id()), - None => None, - }; - - match http_service_id { - Some(instance_id) => { - let http_service = tree - .get_instance_mut(instance_id) - .expect("HttpService has disappeared suddenly"); - http_service - .properties - .entry("HttpEnabled".to_string()) - .or_insert(RbxValue::Bool { value: true }); - } - None => insert_http_service(tree), - } -} - -fn insert_http_service(tree: &mut RbxTree) { - let http_service = RbxInstanceProperties { - name: String::from("HttpService"), - class_name: String::from("HttpService"), - properties: { - let mut properties = HashMap::new(); - - properties.insert(String::from("HttpEnabled"), RbxValue::Bool { value: true }); - - properties - }, - }; - - tree.insert_instance(http_service, tree.get_root_id()); -} diff --git a/src/place_runner.rs b/src/place_runner.rs index c4edee4..3f880c8 100644 --- a/src/place_runner.rs +++ b/src/place_runner.rs @@ -1,20 +1,17 @@ use std::{ - collections::HashMap, - fs::{self, File}, - path::{Path, PathBuf}, + path::PathBuf, process::{self, Command, Stdio}, - str, sync::mpsc, time::Duration, }; -use rbx_dom_weak::{RbxInstanceProperties, RbxTree}; +use anyhow::{anyhow, bail, Context}; +use fs_err as fs; +use fs_err::File; use roblox_install::RobloxStudio; -use tempfile::{tempdir, TempDir}; use crate::{ message_receiver::{Message, MessageReceiver, MessageReceiverOptions, RobloxMessage}, - place::RunInRbxPlace, plugin::RunInRbxPlugin, }; @@ -27,142 +24,71 @@ impl Drop for KillOnDrop { } } -pub struct PlaceRunnerOptions<'a> { - pub port: u16, - pub timeout: u16, - pub lua_script: &'a str, -} - pub struct PlaceRunner { - _work_dir: TempDir, - place_file_path: PathBuf, - plugin_file_path: PathBuf, - studio_exec_path: PathBuf, - port: u16, + pub port: u16, + pub place_path: PathBuf, + pub server_id: String, + pub lua_script: String, } impl PlaceRunner { - pub fn new<'a>(tree: RbxTree, options: PlaceRunnerOptions) -> PlaceRunner { - let work_dir = tempdir().expect("Could not create temporary directory"); - - let place_file_path = work_dir.path().join("place.rbxlx"); - - create_run_in_roblox_place(&place_file_path, tree, options.port); - + pub fn run(&self, sender: mpsc::Sender>) -> Result<(), anyhow::Error> { let studio_install = - RobloxStudio::locate().expect("Could not find Roblox Studio installation"); + RobloxStudio::locate().context("Could not locate a Roblox Studio installation.")?; let plugin_file_path = studio_install .plugins_path() - .join(format!("run_in_roblox-{}.rbxmx", options.port)); + .join(format!("run_in_roblox-{}.rbxmx", self.port)); - create_run_in_roblox_plugin( - &plugin_file_path, - options.port, - options.timeout, - options.lua_script, - ); + let plugin = RunInRbxPlugin { + port: self.port, + server_id: &self.server_id, + lua_script: &self.lua_script, + }; - PlaceRunner { - // Tie the lifetime of this TempDir to our own lifetime, so that it - // doesn't get cleaned up until we're dropped - _work_dir: work_dir, - place_file_path, - plugin_file_path, - studio_exec_path: studio_install.application_path().to_path_buf(), - port: options.port, - } - } + let plugin_file = File::create(&plugin_file_path)?; + plugin.write(plugin_file)?; - pub fn run_with_sender(&self, message_processor: mpsc::Sender>) { - let message_receiver = MessageReceiver::start(MessageReceiverOptions { port: self.port }); + let message_receiver = MessageReceiver::start(MessageReceiverOptions { + port: self.port, + server_id: self.server_id.to_owned(), + }); let _studio_process = KillOnDrop( - Command::new(&self.studio_exec_path) - .arg(format!("{}", self.place_file_path.display())) + Command::new(studio_install.application_path()) + .arg(format!("{}", self.place_path.display())) .stdout(Stdio::null()) .stderr(Stdio::null()) - .spawn() - .expect("Couldn't start Roblox Studio"), + .spawn()?, ); - match message_receiver + let first_message = message_receiver .recv_timeout(Duration::from_secs(20)) - .expect("Timeout reached") - { + .ok_or_else(|| anyhow!("Timeout reached"))?; + + match first_message { Message::Start => {} - _ => panic!("Invalid first message received"), + _ => bail!("Invalid first message received from Roblox Studio plugin"), } loop { - let message = message_receiver.recv(); - match message { + match message_receiver.recv() { Message::Start => {} Message::Stop => { - message_processor - .send(None) - .expect("Could not send stop message"); + sender.send(None)?; break; } Message::Messages(roblox_messages) => { for message in roblox_messages.into_iter() { - message_processor - .send(Some(message)) - .expect("Could not send message"); + sender.send(Some(message))?; } } } } message_receiver.stop(); - let _ignored = fs::remove_file(&self.plugin_file_path); - } -} + fs::remove_file(&plugin_file_path)?; -pub fn open_rbx_place_file(path: &Path, extension: &str) -> RbxTree { - let mut file = File::open(path).expect("Couldn't open file"); - - let mut tree = RbxTree::new(RbxInstanceProperties { - name: String::from("Place"), - class_name: String::from("DataModel"), - properties: HashMap::new(), - }); - let root_id = tree.get_root_id(); - - match extension { - "rbxl" => rbx_binary::decode(&mut tree, root_id, &mut file) - .expect("Couldn't decode binary place file"), - "rbxlx" => { - tree = rbx_xml::from_reader_default(file).expect("Couldn't decode XML place file"); - } - _ => unreachable!(), + Ok(()) } - - tree -} - -fn create_run_in_roblox_place(place_file_path: &PathBuf, tree: RbxTree, port: u16) { - let place_file = File::create(place_file_path).expect("Could not create temporary place file"); - - let place = RunInRbxPlace::new(tree, port); - - place - .write(&place_file) - .expect("Could not serialize temporary place file to disk"); -} - -fn create_run_in_roblox_plugin<'a>( - plugin_file_path: &PathBuf, - port: u16, - timeout: u16, - lua_script: &'a str, -) { - let plugin = RunInRbxPlugin::new(port, timeout, lua_script); - - let plugin_file = - File::create(&plugin_file_path).expect("Could not create temporary plugin file"); - - plugin - .write(plugin_file) - .expect("Could not serialize plugin file to disk"); } diff --git a/src/plugin.rs b/src/plugin.rs index 40aa634..dc48c8f 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -7,46 +7,32 @@ use rbx_dom_weak::{RbxInstanceProperties, RbxTree, RbxValue}; static PLUGIN_TEMPLATE: &'static str = include_str!("plugin_main_template.lua"); pub struct RunInRbxPlugin<'a> { - port: u16, - timeout: u16, - lua_script: &'a str, + pub port: u16, + pub server_id: &'a str, + pub lua_script: &'a str, } impl<'a> RunInRbxPlugin<'a> { - pub fn new(port: u16, timeout: u16, lua_script: &'a str) -> RunInRbxPlugin<'a> { - RunInRbxPlugin { - port, - timeout, - lua_script, - } - } - pub fn write(&self, output: W) -> Result<(), EncodeError> { - let plugin_folder = self.build_plugin(); - let folder_id = plugin_folder.get_root_id(); + let tree = self.build_plugin(); + let root_id = tree.get_root_id(); - rbx_xml::to_writer_default(output, &plugin_folder, &[folder_id]) + rbx_xml::to_writer_default(output, &tree, &[root_id]) } fn build_plugin(&self) -> RbxTree { - let mut plugin_folder = RbxTree::new(RbxInstanceProperties { - name: String::from("run-in-roblox-plugin"), - class_name: String::from("Folder"), - properties: HashMap::new(), - }); - let complete_source = PLUGIN_TEMPLATE .replace("{{PORT}}", &self.port.to_string()) - .replace("{{TIMEOUT}}", &self.timeout.to_string()); + .replace("{{SERVER_ID}}", self.server_id); let plugin_script = RbxInstanceProperties { - name: format!("run-in-roblox-plugin-{}", self.port), - class_name: String::from("Script"), + name: "run-in-roblox-plugin".to_owned(), + class_name: "Script".to_owned(), properties: { let mut properties = HashMap::new(); properties.insert( - String::from("Source"), + "Source".to_owned(), RbxValue::String { value: complete_source, }, @@ -56,66 +42,24 @@ impl<'a> RunInRbxPlugin<'a> { }, }; - let main_source = format!("{}\nreturn nil", self.lua_script); + let main_source = format!("return function()\n{}\nend", self.lua_script); let injected_main = RbxInstanceProperties { - name: String::from("main"), - class_name: String::from("ModuleScript"), + name: "Main".to_owned(), + class_name: "ModuleScript".to_owned(), properties: { let mut properties = HashMap::new(); - properties.insert( - String::from("Source"), - RbxValue::String { value: main_source }, - ); + properties.insert("Source".to_owned(), RbxValue::String { value: main_source }); properties }, }; - let folder_id = plugin_folder.get_root_id(); - let plugin_script_id = plugin_folder.insert_instance(plugin_script, folder_id); - - plugin_folder.insert_instance(injected_main, plugin_script_id); - - plugin_folder - } -} + let mut tree = RbxTree::new(plugin_script); + let root_id = tree.get_root_id(); + tree.insert_instance(injected_main, root_id); -#[cfg(test)] -mod test_plugin { - use super::*; - - #[test] - fn run_in_rbx_plugin_creates_correct_plugin_structure() { - let port = 8080; - let seconds = 100; - let main_script = "print('Done')"; - let plugin = RunInRbxPlugin::new(port, seconds, main_script); - - let plugin_folder = plugin.build_plugin(); - let folder_id = plugin_folder.get_root_id(); - - assert_eq!(plugin_folder.descendants(folder_id).count(), 2); - - let expect_plugin_source = PLUGIN_TEMPLATE - .replace("{{PORT}}", &port.to_string()) - .replace("{{TIMEOUT}}", &seconds.to_string()); - - let expect_main = format!("{}\nreturn nil", main_script); - - for descendant in plugin_folder.descendants(folder_id) { - match descendant.class_name.as_ref() { - "Script" => match descendant.properties.get("Source") { - Some(RbxValue::String { value }) => assert_eq!(value, &expect_plugin_source), - _ => panic!("plugin should have a script with source"), - }, - "ModuleScript" => match descendant.properties.get("Source") { - Some(RbxValue::String { value }) => assert_eq!(value, &expect_main), - _ => panic!("plugin should have the given main source"), - }, - _ => panic!("unexpected descendant"), - } - } + tree } } diff --git a/src/plugin_main_template.lua b/src/plugin_main_template.lua index 15c475b..08efa1d 100644 --- a/src/plugin_main_template.lua +++ b/src/plugin_main_template.lua @@ -1,43 +1,51 @@ -local portValue = game:FindFirstChild("RUN_IN_ROBLOX_PORT") +local PORT = "{{PORT}}" +local SERVER_ID = "{{SERVER_ID}}" -while portValue == nil do - game.ChildAdded:Wait() - portValue = game:FindFirstChild("RUN_IN_ROBLOX_PORT") -end - -if portValue.ClassName ~= "IntValue" then - warn("run-in-roblox found RUN_IN_ROBLOX_PORT marker, but it was the wrong type.") - return -end - -local place_port = portValue.Value +local SERVER_URL = string.format("http://localhost:%s", PORT) local HttpService = game:GetService("HttpService") local LogService = game:GetService("LogService") local RunService = game:GetService("RunService") -local PORT = {{PORT}} -local SERVER_URL = ("http://localhost:%d"):format(PORT) +local pingSuccess, remoteServerId = pcall(function() + return HttpService:GetAsync(SERVER_URL) +end) -if place_port ~= PORT then +-- If there was a transport error, just abort silently. +-- +-- It's possible that the run-in-roblox plugin is erroneously installed, and we +-- should minimize our impact to the user. +if not pingSuccess then + return +end + +-- There is a server running on that port, but it isn't the right run-in-roblox +-- server and might be some other HTTP server. +if remoteServerId ~= SERVER_ID then return end local queuedMessages = {} local timeSinceLastSend = 0 -local messageSendRate = 0.2 -local closeDelay = 0.5 -local running = false +local messageSendRate = 0.1 + +local function flushMessages() + if #queuedMessages == 0 then + return + end + + local encoded = HttpService:JSONEncode(queuedMessages) + queuedMessages = {} + + timeSinceLastSend = 0 + HttpService:PostAsync(SERVER_URL .. "/messages", encoded) +end local heartbeatConnection = RunService.Heartbeat:Connect(function(dt) timeSinceLastSend = timeSinceLastSend + dt - if timeSinceLastSend >= messageSendRate and running then - local encoded = HttpService:JSONEncode(queuedMessages) - queuedMessages = {} - timeSinceLastSend = 0 - - HttpService:PostAsync(SERVER_URL .. "/messages", encoded) + if timeSinceLastSend >= messageSendRate then + flushMessages() end end) @@ -58,36 +66,32 @@ end) HttpService:PostAsync(SERVER_URL .. "/start", "") -running = true +local loadSuccess, messageOrMain = xpcall(require, debug.traceback, script.Main) -spawn(function() - local success, errorMessage = pcall(function() - require(script.main) +if not loadSuccess then + local sacrificialEvent = Instance.new("BindableEvent") + sacrificialEvent.Event:Connect(function() + error(messageOrMain, 0) end) - - if not success then - warn("main encountered an error:") - warn(errorMessage) - end - - wait(closeDelay) - running = false -end) - -local timeout = tick() + {{TIMEOUT}} - -while running and tick() < timeout do - wait(1) + sacrificialEvent:Fire() end -local success, errorMessage = pcall(function() - HttpService:PostAsync(SERVER_URL .. "/stop", "") -end) +local mainSuccess, message = xpcall(messageOrMain, debug.traceback) -if not success then - warn("Could not send POST request to stop") - warn(errorMessage) +if not mainSuccess then + local sacrificialEvent = Instance.new("BindableEvent") + sacrificialEvent.Event:Connect(function() + error(message, 0) + end) + sacrificialEvent:Fire() end +-- Wait for any remaining messages to be sent to LogService, then flush them +-- explicitly. +wait(2 * messageSendRate) heartbeatConnection:Disconnect() logConnection:Disconnect() + +flushMessages() + +HttpService:PostAsync(SERVER_URL .. "/stop", "") \ No newline at end of file