From ec1f9bd706130487f73e277882020112a4a87015 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Sun, 10 Jun 2018 22:59:04 -0700 Subject: [PATCH] merge impl-v2: server --- server/.editorconfig | 5 - server/Cargo.lock | 149 +++---- server/Cargo.toml | 15 +- server/src/bin.rs | 39 +- server/src/commands/serve.rs | 99 +---- server/src/core.rs | 1 - server/src/file_route.rs | 89 +++++ server/src/id.rs | 21 + server/src/lib.rs | 26 ++ server/src/message_session.rs | 64 +++ server/src/partition.rs | 13 + server/src/partition_watcher.rs | 65 +++ server/src/plugin.rs | 60 --- server/src/plugins/default_plugin.rs | 63 --- server/src/plugins/json_model_plugin.rs | 99 ----- server/src/plugins/mod.rs | 7 - server/src/plugins/script_plugin.rs | 121 ------ server/src/project.rs | 219 +++++++---- server/src/rbx.rs | 132 +++++-- server/src/rbx_session.rs | 231 +++++++++++ server/src/session.rs | 95 +++++ server/src/vfs/mod.rs | 7 - server/src/vfs/vfs_item.rs | 36 -- server/src/vfs/vfs_session.rs | 220 ----------- server/src/vfs/vfs_watcher.rs | 108 ----- server/src/vfs_session.rs | 242 ++++++++++++ server/src/web.rs | 371 +++++++++--------- server/src/web_util.rs | 43 ++ server/test-projects/empty/rojo.json | 5 + server/test-projects/one-partition/lib/a.lua | 1 + .../one-partition/lib/a.server.lua | 1 + .../one-partition/lib/b.client.lua | 1 + server/test-projects/one-partition/rojo.json | 10 + server/tests/test_util/mod.rs | 22 ++ server/tests/web.rs | 170 ++++++++ 35 files changed, 1643 insertions(+), 1207 deletions(-) delete mode 100644 server/.editorconfig delete mode 100644 server/src/core.rs create mode 100644 server/src/file_route.rs create mode 100644 server/src/id.rs create mode 100644 server/src/lib.rs create mode 100644 server/src/message_session.rs create mode 100644 server/src/partition.rs create mode 100644 server/src/partition_watcher.rs delete mode 100644 server/src/plugin.rs delete mode 100644 server/src/plugins/default_plugin.rs delete mode 100644 server/src/plugins/json_model_plugin.rs delete mode 100644 server/src/plugins/mod.rs delete mode 100644 server/src/plugins/script_plugin.rs create mode 100644 server/src/rbx_session.rs create mode 100644 server/src/session.rs delete mode 100644 server/src/vfs/mod.rs delete mode 100644 server/src/vfs/vfs_item.rs delete mode 100644 server/src/vfs/vfs_session.rs delete mode 100644 server/src/vfs/vfs_watcher.rs create mode 100644 server/src/vfs_session.rs create mode 100644 server/src/web_util.rs create mode 100644 server/test-projects/empty/rojo.json create mode 100644 server/test-projects/one-partition/lib/a.lua create mode 100644 server/test-projects/one-partition/lib/a.server.lua create mode 100644 server/test-projects/one-partition/lib/b.client.lua create mode 100644 server/test-projects/one-partition/rojo.json create mode 100644 server/tests/test_util/mod.rs create mode 100644 server/tests/web.rs diff --git a/server/.editorconfig b/server/.editorconfig deleted file mode 100644 index 1be11156..00000000 --- a/server/.editorconfig +++ /dev/null @@ -1,5 +0,0 @@ -[*.rs] -indent_style = space -indent_size = 4 -trim_trailing_whitespace = true -insert_final_newline = true \ No newline at end of file diff --git a/server/Cargo.lock b/server/Cargo.lock index a3b0fe3e..f777c0ab 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -26,7 +26,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "atty" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", @@ -55,7 +55,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "bitflags" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -102,7 +102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "cfg-if" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -135,8 +135,8 @@ version = "2.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "atty 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -145,7 +145,7 @@ dependencies = [ [[package]] name = "crc" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -236,7 +236,7 @@ name = "filetime" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -264,7 +264,7 @@ name = "fuchsia-zircon" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -283,7 +283,7 @@ name = "gzip-header" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -299,7 +299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -347,7 +347,7 @@ name = "log" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -385,8 +385,8 @@ version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", - "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", - "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", + "phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_codegen 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -438,7 +438,7 @@ name = "net2" version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -524,33 +524,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "phf" -version = "0.7.21" +version = "0.7.22" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "phf_codegen" -version = "0.7.21" +version = "0.7.22" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", - "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_generator 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "phf_generator" -version = "0.7.21" +version = "0.7.22" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "phf_shared" -version = "0.7.21" +version = "0.7.22" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -559,7 +559,7 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -570,7 +570,7 @@ name = "quote" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -608,19 +608,19 @@ dependencies = [ [[package]] name = "regex" -version = "0.2.10" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "regex-syntax" -version = "0.5.5" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -636,17 +636,18 @@ dependencies = [ [[package]] name = "rojo" -version = "0.4.11" +version = "0.5.0" dependencies = [ "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "rouille 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -662,9 +663,9 @@ dependencies = [ "multipart 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -693,37 +694,27 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.42" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde_derive" -version = "1.0.42" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "serde_derive_internals" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "serde_json" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -748,10 +739,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "syn" -version = "0.13.1" +version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -765,6 +756,18 @@ dependencies = [ "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tempfile" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "term" version = "0.2.14" @@ -863,7 +866,7 @@ dependencies = [ [[package]] name = "unicode-normalization" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -985,23 +988,23 @@ dependencies = [ "checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum ascii 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae7d751998c189c1d4468cf0a39bb2eae052a9c58d50ebb3b9591ee3813ad50" -"checksum atty 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6609a866dd1a1b2d0ee1362195bf3e4f6438abb2d80120b83b1e1f4fb6476dd0" +"checksum atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2fc4a1aa4c24c0718a250f0681885c1af91419d242f29eb8f2ab28502d80dbd1" "checksum base64 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5032d51da2741729bfdaeb2664d9b8c6d9fd1e2b90715c660b6def36628499c2" "checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3" "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" -"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" +"checksum bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789" "checksum brotli-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cb50f54b2e0c671b7ef1637a76237ebacbb293be179440d5d65ca288e42116bb" "checksum brotli2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ea9d0bbab1235017a09226b079ed733bca4bf9ecb6b6102bd01aac79ea082dca" "checksum buf_redux 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b9279646319ff816b05fb5897883ece50d7d854d12b59992683d4f8a71b0f949" "checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" "checksum byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "73b5bdfe7ee3ad0b99c9801d58807a9dbc9e09196365b0203853b99889ab3c87" "checksum bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c129aff112dcc562970abb69e2508b40850dd24c274761bb50fb8a0067ba6c27" -"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" +"checksum cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "405216fd8fe65f718daa7102ea808a946b6ce40c742998fbfd3463645552de18" "checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00" "checksum chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6" "checksum chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "498d20a7aaf62625b9bf26e637cf7736417cde1d0c99f1d04d1170229a85cf87" "checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536" -"checksum crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd5d02c0aac6bd68393ed69e00bbc2457f3e89075c6349db7189618dc4ddc1d7" +"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" "checksum deflate 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)" = "32c8120d981901a9970a3a1c97cf8b630e0fa8c3ca31e75b6fd6fd5f9f427b31" "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" @@ -1046,33 +1049,33 @@ dependencies = [ "checksum num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dee092fcdf725aee04dd7da1d21debff559237d49ef1cb3e69bcb8ece44c7364" "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" -"checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc" -"checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f" -"checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03" -"checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2" -"checksum proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "49b6a521dc81b643e9a51e0d1cf05df46d5a2f3c0280ea72bcb68276ba64a118" +"checksum phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "7d37a244c75a9748e049225155f56dbcb98fe71b192fd25fd23cb914b5ad62f2" +"checksum phf_codegen 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "4e4048fe7dd7a06b8127ecd6d3803149126e9b33c7558879846da3a63f734f2b" +"checksum phf_generator 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "05a079dd052e7b674d21cb31cbb6c05efd56a2cd2827db7692e2f1a507ebd998" +"checksum phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "c2261d544c2bb6aa3b10022b0be371b9c7c64f762ef28c6f5d4f1ef6d97b5930" +"checksum proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7" "checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8" "checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" "checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" -"checksum regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "aec3f58d903a7d2a9dc2bf0e41a746f4530e0cab6b615494e058f67a3ef947fb" -"checksum regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bd90079345f4a4c3409214734ae220fd773c6f2e8a543d07370c6c1c369cfbfb" +"checksum regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75ecf88252dce580404a22444fc7d626c01815debba56a7f4f536772a5ff19d3" +"checksum regex-syntax 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8f1ac0f60d675cc6cf13a20ec076568254472551051ad5dd050364d70671bf6b" "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" "checksum rouille 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc1f8407af80b0630983b2c1f1860dda1960fdec8d3ee75ba8db14937756d3a0" "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" "checksum same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cfb6eded0b06a0b512c8ddbcf04089138c9b4362c2f696f3c3d76039d68f3637" -"checksum serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)" = "a73973861352c932ed1365ce22b32467ce260ac4c8db11cf750ce56334ff2dcf" -"checksum serde_derive 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b392c5a0cebb98121454531c50e60e2ffe0fbeb1a44da277da2d681d08d7dc0b" -"checksum serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9d30c4596450fd7bbda79ef15559683f9a79ac0193ea819db90000d7e1cae794" -"checksum serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "8c6c4e049dc657a99e394bd85c22acbf97356feeec6dbf44150f2dcf79fb3118" +"checksum serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "21924cc18e5281f232a17c040355fac97732b42cf019c24996a1642bcb169cdb" +"checksum serde_derive 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "9c624a90bec6fe9bc60d275d7af71c72c26b24cd6c6776d8e344dc4044caa3e2" +"checksum serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "f3ad6d546e765177cf3dded3c2e424a8040f870083a0e64064746b958ece9cb1" "checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" "checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537" "checksum slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d807fd58c4181bbabed77cb3b891ba9748241a552bcc5be698faaebefc54f46e" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" -"checksum syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "91b52877572087400e83d24b9178488541e3d535259e04ff17a63df1e5ceff59" +"checksum syn 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)" = "61b8f1b737f929c6516ba46a3133fd6d5215ad8a62f66760f851f7048aebedfb" "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +"checksum tempfile 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8cddbd26c5686ece823b507f304c8f188daef548b4cb753512d929ce478a093c" "checksum term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "f2077e54d38055cf1ca0fd7933a2e00cd3ec8f6fed352b2a377f06dcdaaf3281" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" "checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" @@ -1084,7 +1087,7 @@ dependencies = [ "checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d" "checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f" +"checksum unicode-normalization 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "90d662d111b0dbb08a180f2761026cba648c258023c355954a7c00e00e354636" "checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" diff --git a/server/Cargo.toml b/server/Cargo.toml index f44419ea..05bc6d60 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,22 +1,29 @@ [package] name = "rojo" -version = "0.4.11" +version = "0.5.0" authors = ["Lucien Greathouse "] description = "A tool to create robust Roblox projects" license = "MIT" repository = "https://github.com/LPGhatguy/rojo" +[lib] +name = "librojo" +path = "src/lib.rs" + [[bin]] name = "rojo" path = "src/bin.rs" [dependencies] clap = "2.27.1" -rouille = "2.1.0" +rouille = "2.1" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" notify = "4.0.0" -rand = "0.3" -regex = "0.2" +rand = "0.4" +regex = "1.0" lazy_static = "1.0" + +[dev-dependencies] +tempfile = "3" diff --git a/server/src/bin.rs b/server/src/bin.rs index 2546c9aa..bf20429e 100644 --- a/server/src/bin.rs +++ b/server/src/bin.rs @@ -1,27 +1,11 @@ -#[macro_use] extern crate serde_derive; -#[macro_use] extern crate rouille; #[macro_use] extern crate clap; -#[macro_use] extern crate lazy_static; -extern crate notify; -extern crate rand; -extern crate serde; -extern crate serde_json; -extern crate regex; -pub mod web; -pub mod core; -pub mod project; -pub mod pathext; -pub mod vfs; -pub mod rbx; -pub mod plugin; -pub mod plugins; -pub mod commands; +extern crate librojo; use std::path::{Path, PathBuf}; use std::process; -use pathext::canonicalish; +use librojo::pathext::canonicalish; fn main() { let matches = clap_app!(rojo => @@ -40,26 +24,15 @@ fn main() { (@arg port: --port +takes_value "The port to listen on. Defaults to 8000.") ) - (@subcommand pack => - (about: "Packs the project into a GUI installer bundle. NOT YET IMPLEMENTED!") - (@arg PROJECT: "Path to the project to pack. Defaults to the current directory.") - ) - - (@arg verbose: --verbose "Enable extended logging.") ).get_matches(); - let verbose = match matches.occurrences_of("verbose") { - 0 => false, - _ => true, - }; - match matches.subcommand() { ("init", sub_matches) => { let sub_matches = sub_matches.unwrap(); let project_path = Path::new(sub_matches.value_of("PATH").unwrap_or(".")); let full_path = canonicalish(project_path); - commands::init(&full_path); + librojo::commands::init(&full_path); }, ("serve", sub_matches) => { let sub_matches = sub_matches.unwrap(); @@ -82,11 +55,7 @@ fn main() { } }; - commands::serve(&project_path, verbose, port); - }, - ("pack", _) => { - eprintln!("'rojo pack' is not yet implemented!"); - process::exit(1); + librojo::commands::serve(&project_path, port); }, _ => { eprintln!("Please specify a subcommand!"); diff --git a/server/src/commands/serve.rs b/server/src/commands/serve.rs index eb841807..497c4ccb 100644 --- a/server/src/commands/serve.rs +++ b/server/src/commands/serve.rs @@ -1,98 +1,37 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process; -use std::sync::{Arc, Mutex}; -use std::thread; +use std::fs; use rand; -use project::{Project, ProjectLoadError}; -use plugin::{PluginChain}; -use plugins::{DefaultPlugin, JsonModelPlugin, ScriptPlugin}; -use vfs::{VfsSession, VfsWatcher}; -use web; +use project::Project; +use web::{self, WebConfig}; +use session::Session; -pub fn serve(project_path: &PathBuf, verbose: bool, port: Option) { +pub fn serve(project_dir: &PathBuf, override_port: Option) { let server_id = rand::random::(); - let project = match Project::load(project_path) { - Ok(project) => { - println!("Using project \"{}\" from {}", project.name, project_path.display()); - project + let project = match Project::load(project_dir) { + Ok(v) => { + println!("Using project from {}", fs::canonicalize(project_dir).unwrap().display()); + v }, Err(err) => { - match err { - ProjectLoadError::InvalidJson(serde_err) => { - eprintln!("Project contained invalid JSON!"); - eprintln!("{}", project_path.display()); - eprintln!("Error: {}", serde_err); - - process::exit(1); - }, - ProjectLoadError::FailedToOpen | ProjectLoadError::FailedToRead => { - eprintln!("Found project file, but failed to read it!"); - eprintln!("Check the permissions of the project file at {}", project_path.display()); - - process::exit(1); - }, - ProjectLoadError::DidNotExist => { - eprintln!("Found no project file! Create one using 'rojo init'"); - eprintln!("Checked for a project at {}", project_path.display()); - - process::exit(1); - }, - } + eprintln!("{}", err); + process::exit(1); }, }; - if project.partitions.len() == 0 { - println!(""); - println!("This project has no partitions and will not do anything when served!"); - println!("This is usually a mistake -- edit rojo.json!"); - println!(""); - } + let port = override_port.unwrap_or(project.serve_port); - lazy_static! { - static ref PLUGIN_CHAIN: PluginChain = PluginChain::new(vec![ - Box::new(ScriptPlugin::new()), - Box::new(JsonModelPlugin::new()), - Box::new(DefaultPlugin::new()), - ]); - } + println!("Using project {:#?}", project); - let vfs = { - let mut vfs = VfsSession::new(&PLUGIN_CHAIN); + let mut session = Session::new(project.clone()); + session.start(); - for (name, project_partition) in &project.partitions { - let path = { - let given_path = Path::new(&project_partition.path); + let web_config = WebConfig::from_session(server_id, port, &session); - if given_path.is_absolute() { - given_path.to_path_buf() - } else { - project_path.join(given_path) - } - }; + println!("Server listening on port {}", port); - vfs.insert_partition(name, path); - } - - Arc::new(Mutex::new(vfs)) - }; - - { - let vfs = vfs.clone(); - thread::spawn(move || { - VfsWatcher::new(vfs).start(); - }); - } - - let web_config = web::WebConfig { - verbose, - port: port.unwrap_or(project.serve_port), - server_id, - }; - - println!("Server listening on port {}", web_config.port); - - web::start(web_config, project.clone(), &PLUGIN_CHAIN, vfs.clone()); + web::start(web_config); } diff --git a/server/src/core.rs b/server/src/core.rs deleted file mode 100644 index 9259cca0..00000000 --- a/server/src/core.rs +++ /dev/null @@ -1 +0,0 @@ -pub type Route = Vec; diff --git a/server/src/file_route.rs b/server/src/file_route.rs new file mode 100644 index 00000000..ba972bb7 --- /dev/null +++ b/server/src/file_route.rs @@ -0,0 +1,89 @@ +use std::path::{Path, PathBuf, Component}; + +use partition::Partition; + +// TODO: Change backing data structure to use a single allocation with slices +// taken out of it for each portion +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FileRoute { + pub partition: String, + pub route: Vec, +} + +impl FileRoute { + pub fn from_path(path: &Path, partition: &Partition) -> Option { + assert!(path.is_absolute()); + + let relative_path = path.strip_prefix(&partition.path).ok()?; + let mut route = Vec::new(); + + for component in relative_path.components() { + match component { + Component::Normal(piece) => { + route.push(piece.to_string_lossy().into_owned()); + }, + _ => panic!("Unexpected path component: {:?}", component), + } + } + + Some(FileRoute { + partition: partition.name.clone(), + route, + }) + } + + pub fn parent(&self) -> Option { + if self.route.len() == 0 { + return None; + } + + let mut new_route = self.route.clone(); + new_route.pop(); + + Some(FileRoute { + partition: self.partition.clone(), + route: new_route, + }) + } + + /// Creates a PathBuf out of the `FileRoute` based on the given partition + /// `Path`. + pub fn to_path_buf(&self, partition_path: &Path) -> PathBuf { + let mut result = partition_path.to_path_buf(); + + for route_piece in &self.route { + result.push(route_piece); + } + + result + } + + /// Creates a version of the FileRoute with the given extra pieces appended + /// to the end. + pub fn extended_with(&self, pieces: &[&str]) -> FileRoute { + let mut result = self.clone(); + + for piece in pieces { + result.route.push(piece.to_string()); + } + + result + } + + /// This function is totally wrong and should be handled by middleware, heh. + pub fn name(&self, partition: &Partition) -> String { // I guess?? + if self.route.len() == 0 { + // This FileRoute refers to the partition itself + + if partition.target.len() == 0 { + // We're targeting the game! + "game".to_string() + } else { + partition.target.last().unwrap().clone() + } + } else { + // This FileRoute refers to an item in a partition + self.route.last().unwrap().clone() + } + } +} diff --git a/server/src/id.rs b/server/src/id.rs new file mode 100644 index 00000000..a27a9d8c --- /dev/null +++ b/server/src/id.rs @@ -0,0 +1,21 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// A unique identifier, not guaranteed to be generated in any order. +pub type Id = usize; + +lazy_static! { + static ref LAST_ID: AtomicUsize = AtomicUsize::new(0); +} + +/// Generate a new ID, which has no defined ordering. +pub fn get_id() -> Id { + LAST_ID.fetch_add(1, Ordering::SeqCst) +} + +#[test] +fn it_gives_unique_numbers() { + let a = get_id(); + let b = get_id(); + + assert!(a != b); +} diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 00000000..5ea4b8f0 --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,26 @@ +#[macro_use] extern crate serde_derive; +#[macro_use] extern crate rouille; +#[macro_use] extern crate lazy_static; +extern crate notify; +extern crate rand; +extern crate serde; +extern crate serde_json; +extern crate regex; + +#[cfg(test)] +extern crate tempfile; + +pub mod commands; +pub mod file_route; +pub mod id; +pub mod message_session; +pub mod partition; +pub mod partition_watcher; +pub mod pathext; +pub mod project; +pub mod rbx; +pub mod rbx_session; +pub mod session; +pub mod vfs_session; +pub mod web; +pub mod web_util; diff --git a/server/src/message_session.rs b/server/src/message_session.rs new file mode 100644 index 00000000..85b2aa32 --- /dev/null +++ b/server/src/message_session.rs @@ -0,0 +1,64 @@ +use std::collections::HashMap; +use std::sync::{mpsc, Arc, RwLock, Mutex}; + +use id::{Id, get_id}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Message { + InstanceChanged { + id: Id, + }, +} + +#[derive(Clone)] +pub struct MessageSession { + pub messages: Arc>>, + pub message_listeners: Arc>>>, +} + +impl MessageSession { + pub fn new() -> MessageSession { + MessageSession { + messages: Arc::new(RwLock::new(Vec::new())), + message_listeners: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub fn push_messages(&self, new_messages: &[Message]) { + let message_listeners = self.message_listeners.lock().unwrap(); + + { + let mut messages = self.messages.write().unwrap(); + messages.extend_from_slice(new_messages); + } + + { + for listener in message_listeners.values() { + listener.send(()).unwrap(); + } + } + } + + pub fn subscribe(&self, sender: mpsc::Sender<()>) -> Id { + let id = get_id(); + + { + let mut message_listeners = self.message_listeners.lock().unwrap(); + message_listeners.insert(id, sender); + } + + id + } + + pub fn unsubscribe(&self, id: Id) { + { + let mut message_listeners = self.message_listeners.lock().unwrap(); + message_listeners.remove(&id); + } + } + + pub fn get_message_cursor(&self) -> i32 { + self.messages.read().unwrap().len() as i32 - 1 + } +} diff --git a/server/src/partition.rs b/server/src/partition.rs new file mode 100644 index 00000000..9ef66664 --- /dev/null +++ b/server/src/partition.rs @@ -0,0 +1,13 @@ +use std::path::PathBuf; + +#[derive(Debug, Clone, PartialEq)] +pub struct Partition { + /// The unique name of this partition, used for debugging. + pub name: String, + + /// The path on the filesystem that this partition maps to. + pub path: PathBuf, + + /// The route to the Roblox instance that this partition maps to. + pub target: Vec, +} diff --git a/server/src/partition_watcher.rs b/server/src/partition_watcher.rs new file mode 100644 index 00000000..2f9785e7 --- /dev/null +++ b/server/src/partition_watcher.rs @@ -0,0 +1,65 @@ +use std::sync::mpsc::{channel, Sender}; +use std::time::Duration; +use std::thread; + +use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher, watcher}; + +use partition::Partition; +use vfs_session::FileChange; +use file_route::FileRoute; + +pub struct PartitionWatcher { + pub watcher: RecommendedWatcher, +} + +impl PartitionWatcher { + pub fn start_new(partition: Partition, tx: Sender) -> PartitionWatcher { + let (watch_tx, watch_rx) = channel(); + + let mut watcher = watcher(watch_tx, Duration::from_millis(100)).unwrap(); + + watcher.watch(&partition.path, RecursiveMode::Recursive).unwrap(); + + thread::spawn(move || { + loop { + match watch_rx.recv() { + Ok(event) => { + let file_change = match event { + DebouncedEvent::Create(path) => { + let route = FileRoute::from_path(&path, &partition).unwrap(); + FileChange::Created(route) + }, + DebouncedEvent::Write(path) => { + let route = FileRoute::from_path(&path, &partition).unwrap(); + FileChange::Updated(route) + }, + DebouncedEvent::Remove(path) => { + let route = FileRoute::from_path(&path, &partition).unwrap(); + FileChange::Deleted(route) + }, + DebouncedEvent::Rename(from_path, to_path) => { + let from_route = FileRoute::from_path(&from_path, &partition).unwrap(); + let to_route = FileRoute::from_path(&to_path, &partition).unwrap(); + FileChange::Moved(from_route, to_route) + }, + _ => continue, + }; + + match tx.send(file_change) { + Ok(_) => {}, + Err(_) => break, + } + }, + Err(_) => break, + }; + } + }); + + PartitionWatcher { + watcher, + } + } + + pub fn stop(self) { + } +} diff --git a/server/src/plugin.rs b/server/src/plugin.rs deleted file mode 100644 index 82999535..00000000 --- a/server/src/plugin.rs +++ /dev/null @@ -1,60 +0,0 @@ -use rbx::RbxInstance; -use vfs::VfsItem; -use core::Route; - -pub enum TransformFileResult { - Value(Option), - Pass, - - // TODO: Error case -} - -pub enum FileChangeResult { - MarkChanged(Option>), - Pass, -} - -pub trait Plugin { - /// Invoked when a file is read from the filesystem and needs to be turned - /// into a Roblox instance. - fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult; - - /// Invoked when a file changes on the filesystem. The result defines what - /// routes are marked as needing to be refreshed. - fn handle_file_change(&self, route: &Route) -> FileChangeResult; -} - -/// A set of plugins that are composed in order. -pub struct PluginChain { - plugins: Vec>, -} - -impl PluginChain { - pub fn new(plugins: Vec>) -> PluginChain { - PluginChain { - plugins, - } - } - - pub fn transform_file(&self, vfs_item: &VfsItem) -> Option { - for plugin in &self.plugins { - match plugin.transform_file(self, vfs_item) { - TransformFileResult::Value(rbx_item) => return rbx_item, - TransformFileResult::Pass => {}, - } - } - - None - } - - pub fn handle_file_change(&self, route: &Route) -> Option> { - for plugin in &self.plugins { - match plugin.handle_file_change(route) { - FileChangeResult::MarkChanged(changes) => return changes, - FileChangeResult::Pass => {}, - } - } - - None - } -} diff --git a/server/src/plugins/default_plugin.rs b/server/src/plugins/default_plugin.rs deleted file mode 100644 index 6d2866d3..00000000 --- a/server/src/plugins/default_plugin.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::collections::HashMap; - -use core::Route; -use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult}; -use rbx::{RbxInstance, RbxValue}; -use vfs::VfsItem; - -/// A plugin with simple transforms: -/// * Directories become Folder instances -/// * Files become StringValue objects with 'Value' as their contents -pub struct DefaultPlugin; - -impl DefaultPlugin { - pub fn new() -> DefaultPlugin { - DefaultPlugin - } -} - -impl Plugin for DefaultPlugin { - fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult { - match vfs_item { - &VfsItem::File { ref contents, .. } => { - let mut properties = HashMap::new(); - - properties.insert("Value".to_string(), RbxValue::String { - value: contents.clone(), - }); - - TransformFileResult::Value(Some(RbxInstance { - name: vfs_item.name().clone(), - class_name: "StringValue".to_string(), - children: Vec::new(), - properties, - route: Some(vfs_item.route().to_vec()), - })) - }, - &VfsItem::Dir { ref children, .. } => { - let mut rbx_children = Vec::new(); - - for (_, child_item) in children { - match plugins.transform_file(child_item) { - Some(rbx_item) => { - rbx_children.push(rbx_item); - }, - _ => {}, - } - } - - TransformFileResult::Value(Some(RbxInstance { - name: vfs_item.name().clone(), - class_name: "*".to_string(), - children: rbx_children, - properties: HashMap::new(), - route: Some(vfs_item.route().to_vec()), - })) - }, - } - } - - fn handle_file_change(&self, route: &Route) -> FileChangeResult { - FileChangeResult::MarkChanged(Some(vec![route.clone()])) - } -} diff --git a/server/src/plugins/json_model_plugin.rs b/server/src/plugins/json_model_plugin.rs deleted file mode 100644 index 575feacb..00000000 --- a/server/src/plugins/json_model_plugin.rs +++ /dev/null @@ -1,99 +0,0 @@ -use regex::Regex; -use serde_json; - -use core::Route; -use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult}; -use rbx::RbxInstance; -use vfs::VfsItem; - -lazy_static! { - static ref JSON_MODEL_PATTERN: Regex = Regex::new(r"^(.*?)\.model\.json$").unwrap(); -} - -static JSON_MODEL_INIT: &'static str = "init.model.json"; - -pub struct JsonModelPlugin; - -impl JsonModelPlugin { - pub fn new() -> JsonModelPlugin { - JsonModelPlugin - } -} - -impl Plugin for JsonModelPlugin { - fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult { - match vfs_item { - &VfsItem::File { ref contents, .. } => { - let rbx_name = match JSON_MODEL_PATTERN.captures(vfs_item.name()) { - Some(captures) => captures.get(1).unwrap().as_str().to_string(), - None => return TransformFileResult::Pass, - }; - - let mut rbx_item: RbxInstance = match serde_json::from_str(contents) { - Ok(v) => v, - Err(e) => { - eprintln!("Unable to parse JSON Model File named {}: {}", vfs_item.name(), e); - - return TransformFileResult::Pass; // This should be an error in the future! - }, - }; - - rbx_item.route = Some(vfs_item.route().to_vec()); - rbx_item.name = rbx_name; - - TransformFileResult::Value(Some(rbx_item)) - }, - &VfsItem::Dir { ref children, .. } => { - let init_item = match children.get(JSON_MODEL_INIT) { - Some(v) => v, - None => return TransformFileResult::Pass, - }; - - let mut rbx_item = match self.transform_file(plugins, init_item) { - TransformFileResult::Value(Some(item)) => item, - TransformFileResult::Value(None) | TransformFileResult::Pass => { - eprintln!("Inconsistency detected in JsonModelPlugin!"); - return TransformFileResult::Pass; - }, - }; - - rbx_item.name.clear(); - rbx_item.name.push_str(vfs_item.name()); - rbx_item.route = Some(vfs_item.route().to_vec()); - - for (child_name, child_item) in children { - if child_name == init_item.name() { - continue; - } - - match plugins.transform_file(child_item) { - Some(child_rbx_item) => { - rbx_item.children.push(child_rbx_item); - }, - _ => {}, - } - } - - TransformFileResult::Value(Some(rbx_item)) - }, - } - } - - fn handle_file_change(&self, route: &Route) -> FileChangeResult { - let leaf = match route.last() { - Some(v) => v, - None => return FileChangeResult::Pass, - }; - - let is_init = leaf == JSON_MODEL_INIT; - - if is_init { - let mut changed = route.clone(); - changed.pop(); - - FileChangeResult::MarkChanged(Some(vec![changed])) - } else { - FileChangeResult::Pass - } - } -} diff --git a/server/src/plugins/mod.rs b/server/src/plugins/mod.rs deleted file mode 100644 index 73482635..00000000 --- a/server/src/plugins/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod default_plugin; -mod script_plugin; -mod json_model_plugin; - -pub use self::default_plugin::*; -pub use self::script_plugin::*; -pub use self::json_model_plugin::*; diff --git a/server/src/plugins/script_plugin.rs b/server/src/plugins/script_plugin.rs deleted file mode 100644 index 185f1758..00000000 --- a/server/src/plugins/script_plugin.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::collections::HashMap; - -use regex::Regex; - -use core::Route; -use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult}; -use rbx::{RbxInstance, RbxValue}; -use vfs::VfsItem; - -lazy_static! { - static ref SERVER_PATTERN: Regex = Regex::new(r"^(.*?)\.server\.lua$").unwrap(); - static ref CLIENT_PATTERN: Regex = Regex::new(r"^(.*?)\.client\.lua$").unwrap(); - static ref MODULE_PATTERN: Regex = Regex::new(r"^(.*?)\.lua$").unwrap(); -} - -static SERVER_INIT: &'static str = "init.server.lua"; -static CLIENT_INIT: &'static str = "init.client.lua"; -static MODULE_INIT: &'static str = "init.lua"; - -pub struct ScriptPlugin; - -impl ScriptPlugin { - pub fn new() -> ScriptPlugin { - ScriptPlugin - } -} - -impl Plugin for ScriptPlugin { - fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult { - match vfs_item { - &VfsItem::File { ref contents, .. } => { - let name = vfs_item.name(); - - let (class_name, rbx_name) = { - if let Some(captures) = SERVER_PATTERN.captures(name) { - ("Script".to_string(), captures.get(1).unwrap().as_str().to_string()) - } else if let Some(captures) = CLIENT_PATTERN.captures(name) { - ("LocalScript".to_string(), captures.get(1).unwrap().as_str().to_string()) - } else if let Some(captures) = MODULE_PATTERN.captures(name) { - ("ModuleScript".to_string(), captures.get(1).unwrap().as_str().to_string()) - } else { - return TransformFileResult::Pass; - } - }; - - let mut properties = HashMap::new(); - - properties.insert("Source".to_string(), RbxValue::String { - value: contents.clone(), - }); - - TransformFileResult::Value(Some(RbxInstance { - name: rbx_name, - class_name: class_name, - children: Vec::new(), - properties, - route: Some(vfs_item.route().to_vec()), - })) - }, - &VfsItem::Dir { ref children, .. } => { - let init_item = { - let maybe_item = children.get(SERVER_INIT) - .or(children.get(CLIENT_INIT)) - .or(children.get(MODULE_INIT)); - - match maybe_item { - Some(v) => v, - None => return TransformFileResult::Pass, - } - }; - - let mut rbx_item = match self.transform_file(plugins, init_item) { - TransformFileResult::Value(Some(item)) => item, - _ => { - eprintln!("Inconsistency detected in ScriptPlugin!"); - return TransformFileResult::Pass; - }, - }; - - rbx_item.name.clear(); - rbx_item.name.push_str(vfs_item.name()); - rbx_item.route = Some(vfs_item.route().to_vec()); - - for (child_name, child_item) in children { - if child_name == init_item.name() { - continue; - } - - match plugins.transform_file(child_item) { - Some(child_rbx_item) => { - rbx_item.children.push(child_rbx_item); - }, - _ => {}, - } - } - - TransformFileResult::Value(Some(rbx_item)) - }, - } - } - - fn handle_file_change(&self, route: &Route) -> FileChangeResult { - let leaf = match route.last() { - Some(v) => v, - None => return FileChangeResult::Pass, - }; - - let is_init = leaf == SERVER_INIT - || leaf == CLIENT_INIT - || leaf == MODULE_INIT; - - if is_init { - let mut changed = route.clone(); - changed.pop(); - - FileChangeResult::MarkChanged(Some(vec![changed])) - } else { - FileChangeResult::Pass - } - } -} diff --git a/server/src/project.rs b/server/src/project.rs index eb45ce21..b0b95de7 100644 --- a/server/src/project.rs +++ b/server/src/project.rs @@ -2,18 +2,39 @@ use std::collections::HashMap; use std::fmt; use std::fs::{self, File}; use std::io::{Read, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; + +use rand::{self, Rng}; use serde_json; +use partition::Partition; + pub static PROJECT_FILENAME: &'static str = "rojo.json"; #[derive(Debug)] pub enum ProjectLoadError { - DidNotExist, - FailedToOpen, - FailedToRead, - InvalidJson(serde_json::Error), + DidNotExist(PathBuf), + FailedToOpen(PathBuf), + FailedToRead(PathBuf), + InvalidJson(PathBuf, serde_json::Error), +} + +impl fmt::Display for ProjectLoadError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &ProjectLoadError::InvalidJson(ref project_path, ref serde_err) => { + write!(f, "Found invalid JSON reading project: {}\nError: {}", project_path.display(), serde_err) + }, + &ProjectLoadError::FailedToOpen(ref project_path) | + &ProjectLoadError::FailedToRead(ref project_path) => { + write!(f, "Found project file, but failed to read it: {}", project_path.display()) + }, + &ProjectLoadError::DidNotExist(ref project_path) => { + write!(f, "Could not locate a project file at {}.\nUse 'rojo init' to create one.", project_path.display()) + }, + } + } } #[derive(Debug)] @@ -34,16 +55,17 @@ impl fmt::Display for ProjectInitError { &ProjectInitError::AlreadyExists => { write!(f, "A project already exists at that location.") }, - &ProjectInitError::FailedToCreate | &ProjectInitError::FailedToWrite => { + &ProjectInitError::FailedToCreate | + &ProjectInitError::FailedToWrite => { write!(f, "Failed to write to the given location.") }, } } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ProjectPartition { +pub struct SourceProjectPartition { /// A slash-separated path to a file or folder, relative to the project's /// directory. pub path: String, @@ -52,43 +74,114 @@ pub struct ProjectPartition { pub target: String, } -/// Represents a project configured by a user for use with Rojo. Holds anything -/// that can be configured with `rojo.json`. -/// -/// In the future, this object will hold dependency information and other handy -/// configurables -#[derive(Clone, Debug, Serialize, Deserialize)] +/// Represents a Rojo project in the format that's most convenient for users to +/// edit. This should generally line up with `Project`, but can diverge when +/// there's either compatibility shims or when the data structures that Rojo +/// want are too verbose to write in JSON but easy to convert from something +/// else. +// +/// Holds anything that can be configured with `rojo.json`. +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] -pub struct Project { +pub struct SourceProject { pub name: String, pub serve_port: u64, - pub partitions: HashMap, + pub partitions: HashMap, +} + +impl Default for SourceProject { + fn default() -> SourceProject { + SourceProject { + name: "new-project".to_string(), + serve_port: 8000, + partitions: HashMap::new(), + } + } +} + +/// Represents a Rojo project in the format that's convenient for Rojo to work +/// with. +#[derive(Debug, Clone)] +pub struct Project { + /// The path to the project file that this project is associated with. + pub project_path: PathBuf, + + /// The name of this project, used for user-facing labels. + pub name: String, + + /// The port that this project will run a web server on. + pub serve_port: u64, + + /// All of the project's partitions, laid out in an expanded way. + pub partitions: HashMap, } impl Project { - /// Creates a new empty Project object with the given name. - pub fn new>(name: T) -> Project { + fn from_source_project(source_project: SourceProject, project_path: PathBuf) -> Project { + let mut partitions = HashMap::new(); + + { + let project_directory = project_path.parent().unwrap(); + + for (partition_name, partition) in source_project.partitions.into_iter() { + let path = project_directory.join(&partition.path); + let target = partition.target + .split(".") + .map(String::from) + .collect::>(); + + partitions.insert(partition_name.clone(), Partition { + path, + target, + name: partition_name, + }); + } + } + Project { - name: name.into(), - ..Default::default() + project_path, + name: source_project.name, + serve_port: source_project.serve_port, + partitions, + } + } + + fn as_source_project(&self) -> SourceProject { + let mut partitions = HashMap::new(); + + for partition in self.partitions.values() { + let path = partition.path.strip_prefix(&self.project_path) + .unwrap_or_else(|_| &partition.path) + .to_str() + .unwrap() + .to_string(); + + let target = partition.target.join("."); + + partitions.insert(partition.name.clone(), SourceProjectPartition { + path, + target, + }); + } + + SourceProject { + partitions, + name: self.name.clone(), + serve_port: self.serve_port, } } /// Initializes a new project inside the given folder path. pub fn init>(location: T) -> Result { let location = location.as_ref(); - let package_path = location.join(PROJECT_FILENAME); + let project_path = location.join(PROJECT_FILENAME); // We abort if the project file already exists. - match fs::metadata(&package_path) { - Ok(_) => return Err(ProjectInitError::AlreadyExists), - Err(_) => {}, - } + fs::metadata(&project_path) + .map_err(|_| ProjectInitError::AlreadyExists)?; - let mut file = match File::create(&package_path) { - Ok(f) => f, - Err(_) => return Err(ProjectInitError::FailedToCreate), - }; + let mut file = File::create(&project_path) + .map_err(|_| ProjectInitError::FailedToCreate)?; // Try to give the project a meaningful name. // If we can't, we'll just fall back to a default. @@ -97,69 +190,57 @@ impl Project { None => "new-project".to_string(), }; + // Generate a random port to run the server on. + let serve_port = rand::thread_rng().gen_range(2000, 49151); + // Configure the project with all of the values we know so far. - let project = Project::new(name); - let serialized = serde_json::to_string_pretty(&project).unwrap(); + let source_project = SourceProject { + name, + serve_port, + partitions: HashMap::new(), + }; + let serialized = serde_json::to_string_pretty(&source_project).unwrap(); - match file.write(serialized.as_bytes()) { - Ok(_) => {}, - Err(_) => return Err(ProjectInitError::FailedToWrite), - } + file.write(serialized.as_bytes()) + .map_err(|_| ProjectInitError::FailedToWrite)?; - Ok(project) + Ok(Project::from_source_project(source_project, project_path)) } /// Attempts to load a project from the file named PROJECT_FILENAME from the /// given folder. pub fn load>(location: T) -> Result { - let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME)); + let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME)); - match fs::metadata(&package_path) { - Ok(_) => {}, - Err(_) => return Err(ProjectLoadError::DidNotExist), - } + fs::metadata(&project_path) + .map_err(|_| ProjectLoadError::DidNotExist(project_path.clone()))?; - let mut file = match File::open(&package_path) { - Ok(f) => f, - Err(_) => return Err(ProjectLoadError::FailedToOpen), - }; + let mut file = File::open(&project_path) + .map_err(|_| ProjectLoadError::FailedToOpen(project_path.clone()))?; let mut contents = String::new(); - match file.read_to_string(&mut contents) { - Ok(_) => {}, - Err(_) => return Err(ProjectLoadError::FailedToRead), - } + file.read_to_string(&mut contents) + .map_err(|_| ProjectLoadError::FailedToRead(project_path.clone()))?; - match serde_json::from_str(&contents) { - Ok(v) => Ok(v), - Err(e) => return Err(ProjectLoadError::InvalidJson(e)), - } + let source_project = serde_json::from_str(&contents) + .map_err(|e| ProjectLoadError::InvalidJson(project_path.clone(), e))?; + + Ok(Project::from_source_project(source_project, project_path)) } /// Saves the given project file to the given folder with the appropriate name. pub fn save>(&self, location: T) -> Result<(), ProjectSaveError> { - let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME)); + let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME)); - let mut file = match File::create(&package_path) { - Ok(f) => f, - Err(_) => return Err(ProjectSaveError::FailedToCreate), - }; + let mut file = File::create(&project_path) + .map_err(|_| ProjectSaveError::FailedToCreate)?; - let serialized = serde_json::to_string_pretty(self).unwrap(); + let source_project = self.as_source_project(); + let serialized = serde_json::to_string_pretty(&source_project).unwrap(); file.write(serialized.as_bytes()).unwrap(); Ok(()) } } - -impl Default for Project { - fn default() -> Project { - Project { - name: "new-project".to_string(), - serve_port: 8000, - partitions: HashMap::new(), - } - } -} diff --git a/server/src/rbx.rs b/server/src/rbx.rs index 14d3b8cd..75c03a1a 100644 --- a/server/src/rbx.rs +++ b/server/src/rbx.rs @@ -1,38 +1,116 @@ +use std::borrow::Cow; use std::collections::HashMap; -/// Represents data about a Roblox instance -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] +use id::Id; + +// TODO: Switch to enum to represent more value types +pub type RbxValue = String; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct RbxInstance { + /// Maps to the `Name` property on Instance. pub name: String, + + /// Maps to the `ClassName` property on Instance. pub class_name: String, - #[serde(default = "Vec::new")] - pub children: Vec, - - #[serde(default = "HashMap::new")] + /// Contains all other properties of an Instance. pub properties: HashMap, - /// The route that this instance was generated from, if there was one. - pub route: Option>, + /// All of the children of this instance. Order is relevant to preserve! + pub children: Vec, + + pub parent: Option, } -/// Any kind value that can be used by Roblox -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase", tag = "Type")] -pub enum RbxValue { - #[serde(rename_all = "PascalCase")] - String { - value: String, - }, - #[serde(rename_all = "PascalCase")] - Bool { - value: bool, - }, - #[serde(rename_all = "PascalCase")] - Number { - value: f64, - }, - - // TODO: Compound types like Vector3 +// This seems like a really bad idea? +// Why isn't there a blanket impl for this for all T? +impl<'a> From<&'a RbxInstance> for Cow<'a, RbxInstance> { + fn from(instance: &'a RbxInstance) -> Cow<'a, RbxInstance> { + Cow::Borrowed(instance) + } +} + +pub struct RbxTree { + instances: HashMap, +} + +impl RbxTree { + pub fn new() -> RbxTree { + RbxTree { + instances: HashMap::new(), + } + } + + pub fn get_all_instances(&self) -> &HashMap { + &self.instances + } + + pub fn insert_instance(&mut self, id: Id, instance: RbxInstance) { + if let Some(parent_id) = instance.parent { + if let Some(mut parent) = self.instances.get_mut(&parent_id) { + if !parent.children.contains(&id) { + parent.children.push(id); + } + } + } + + self.instances.insert(id, instance); + } + + pub fn delete_instance(&mut self, id: Id) -> Vec { + let mut ids_to_visit = vec![id]; + let mut ids_deleted = Vec::new(); + + for instance in self.instances.values_mut() { + match instance.children.iter().position(|&v| v == id) { + Some(index) => { + instance.children.remove(index); + }, + None => {}, + } + } + + loop { + let id = match ids_to_visit.pop() { + Some(id) => id, + None => break, + }; + + match self.instances.get(&id) { + Some(instance) => ids_to_visit.extend_from_slice(&instance.children), + None => continue, + } + + self.instances.remove(&id); + ids_deleted.push(id); + } + + ids_deleted + } + + pub fn get_instance<'a, 'b, T>(&'a self, id: Id, output: &'b mut HashMap) + where T: From<&'a RbxInstance> + { + let mut ids_to_visit = vec![id]; + + loop { + let id = match ids_to_visit.pop() { + Some(id) => id, + None => break, + }; + + match self.instances.get(&id) { + Some(instance) => { + output.insert(id, instance.into()); + + for child_id in &instance.children { + ids_to_visit.push(*child_id); + } + }, + None => continue, + } + } + } } diff --git a/server/src/rbx_session.rs b/server/src/rbx_session.rs new file mode 100644 index 00000000..7db35bff --- /dev/null +++ b/server/src/rbx_session.rs @@ -0,0 +1,231 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use file_route::FileRoute; +use id::{Id, get_id}; +use message_session::{Message, MessageSession}; +use partition::Partition; +use project::Project; +use rbx::{RbxInstance, RbxTree}; +use vfs_session::{VfsSession, FileItem, FileChange}; + +// TODO: Rethink data structure and insertion/update behavior. Maybe break some +// pieces off into a new object? +fn file_to_instances( + file_item: &FileItem, + partition: &Partition, + tree: &mut RbxTree, + instances_by_route: &mut HashMap, + parent_id: Option, +) -> (Id, Vec) { + match file_item { + FileItem::File { contents, route } => { + let primary_id = match instances_by_route.get(&file_item.get_route()) { + Some(&id) => id, + None => { + let id = get_id(); + instances_by_route.insert(route.clone(), id); + + id + }, + }; + + // This is placeholder logic; this whole function is! + let (class_name, property_key, name) = { + // TODO: Root instances have an empty route + let file_name = route.route.last().unwrap(); + + fn strip_suffix<'a>(source: &'a str, suffix: &'static str) -> &'a str { + &source[..source.len() - suffix.len()] + } + + if file_name.ends_with(".client.lua") { + ("LocalScript", "Source", strip_suffix(&file_name, ".client.lua")) + } else if file_name.ends_with(".server.lua") { + ("Script", "Source", strip_suffix(&file_name, ".server.lua")) + } else if file_name.ends_with(".lua") { + ("ModuleScript", "Source", strip_suffix(&file_name, ".lua")) + } else { + // TODO: Error/warn/skip instead of falling back + ("StringValue", "Value", file_name.as_str()) + } + }; + + let mut properties = HashMap::new(); + properties.insert(property_key.to_string(), contents.clone()); + + tree.insert_instance(primary_id, RbxInstance { + name: name.to_string(), + class_name: class_name.to_string(), + properties, + children: Vec::new(), + parent: parent_id, + }); + + (primary_id, vec![primary_id]) + }, + FileItem::Directory { children, route } => { + let primary_id = match instances_by_route.get(&file_item.get_route()) { + Some(&id) => id, + None => { + let id = get_id(); + instances_by_route.insert(route.clone(), id); + + id + }, + }; + + let mut child_ids = Vec::new(); + + let mut changed_ids = vec![primary_id]; + + for child_file_item in children.values() { + let (child_id, mut child_changed_ids) = file_to_instances(child_file_item, partition, tree, instances_by_route, Some(primary_id)); + + child_ids.push(child_id); + changed_ids.push(child_id); + + // TODO: Should I stop using drain on Vecs of Copyable types? + for id in child_changed_ids.drain(..) { + changed_ids.push(id); + } + } + + tree.insert_instance(primary_id, RbxInstance { + name: route.name(partition).to_string(), + class_name: "Folder".to_string(), + properties: HashMap::new(), + children: child_ids, + parent: parent_id, + }); + + (primary_id, changed_ids) + }, + } +} + +pub struct RbxSession { + project: Project, + + vfs_session: Arc>, + + message_session: MessageSession, + + /// The RbxInstance that represents each partition. + // TODO: Can this be removed in favor of instances_by_route? + pub partition_instances: HashMap, + + /// Keeps track of all of the instances in the tree + pub tree: RbxTree, + + /// A map from files in the VFS to instances loaded in the session. + instances_by_route: HashMap, +} + +impl RbxSession { + pub fn new(project: Project, vfs_session: Arc>, message_session: MessageSession) -> RbxSession { + RbxSession { + project, + vfs_session, + message_session, + partition_instances: HashMap::new(), + tree: RbxTree::new(), + instances_by_route: HashMap::new(), + } + } + + pub fn read_partitions(&mut self) { + let vfs_session_arc = self.vfs_session.clone(); + let vfs_session = vfs_session_arc.read().unwrap(); + + for partition in self.project.partitions.values() { + let route = FileRoute { + partition: partition.name.clone(), + route: Vec::new(), + }; + let file_item = vfs_session.get_by_route(&route).unwrap(); + + let parent_id = match route.parent() { + Some(parent_route) => match self.instances_by_route.get(&parent_route) { + Some(&parent_id) => Some(parent_id), + None => None, + }, + None => None, + }; + + let (root_id, _) = file_to_instances(file_item, partition, &mut self.tree, &mut self.instances_by_route, parent_id); + + self.partition_instances.insert(partition.name.clone(), root_id); + } + } + + pub fn handle_change(&mut self, change: &FileChange) { + let vfs_session_arc = self.vfs_session.clone(); + let vfs_session = vfs_session_arc.read().unwrap(); + + match change { + FileChange::Created(route) | FileChange::Updated(route) => { + let file_item = vfs_session.get_by_route(route).unwrap(); + let partition = self.project.partitions.get(&route.partition).unwrap(); + + let parent_id = match route.parent() { + Some(parent_route) => match self.instances_by_route.get(&parent_route) { + Some(&parent_id) => Some(parent_id), + None => None, + }, + None => None, + }; + + let (_, changed_ids) = file_to_instances(file_item, partition, &mut self.tree, &mut self.instances_by_route, parent_id); + + let messages = changed_ids + .iter() + .map(|&id| Message::InstanceChanged { id }) + .collect::>(); + + self.message_session.push_messages(&messages); + }, + FileChange::Deleted(route) => { + match self.instances_by_route.get(route) { + Some(&id) => { + self.tree.delete_instance(id); + self.instances_by_route.remove(route); + self.message_session.push_messages(&[Message::InstanceChanged { id }]); + }, + None => (), + } + }, + FileChange::Moved(from_route, to_route) => { + let mut messages = Vec::new(); + + match self.instances_by_route.get(from_route) { + Some(&id) => { + self.tree.delete_instance(id); + self.instances_by_route.remove(from_route); + messages.push(Message::InstanceChanged { id }); + }, + None => (), + } + + let file_item = vfs_session.get_by_route(to_route).unwrap(); + let partition = self.project.partitions.get(&to_route.partition).unwrap(); + + let parent_id = match to_route.parent() { + Some(parent_route) => match self.instances_by_route.get(&parent_route) { + Some(&parent_id) => Some(parent_id), + None => None, + }, + None => None, + }; + + let (_, changed_ids) = file_to_instances(file_item, partition, &mut self.tree, &mut self.instances_by_route, parent_id); + + for id in changed_ids { + messages.push(Message::InstanceChanged { id }); + } + + self.message_session.push_messages(&messages); + }, + } + } +} diff --git a/server/src/session.rs b/server/src/session.rs new file mode 100644 index 00000000..55341bb3 --- /dev/null +++ b/server/src/session.rs @@ -0,0 +1,95 @@ +use std::sync::{mpsc, Arc, RwLock}; +use std::thread; + +use message_session::MessageSession; +use partition_watcher::PartitionWatcher; +use project::Project; +use rbx_session::RbxSession; +use vfs_session::VfsSession; + +/// Stub trait for middleware +trait Middleware { +} + +pub struct Session { + pub project: Project, + vfs_session: Arc>, + rbx_session: Arc>, + message_session: MessageSession, + watchers: Vec, +} + +impl Session { + pub fn new(project: Project) -> Session { + let message_session = MessageSession::new(); + let vfs_session = Arc::new(RwLock::new(VfsSession::new(project.clone()))); + let rbx_session = Arc::new(RwLock::new(RbxSession::new(project.clone(), vfs_session.clone(), message_session.clone()))); + + Session { + vfs_session, + rbx_session, + watchers: Vec::new(), + message_session, + project, + } + } + + pub fn start(&mut self) { + { + let mut vfs_session = self.vfs_session.write().unwrap(); + vfs_session.read_partitions(); + } + + { + let mut rbx_session = self.rbx_session.write().unwrap(); + rbx_session.read_partitions(); + } + + let (tx, rx) = mpsc::channel(); + + for partition in self.project.partitions.values() { + let watcher = PartitionWatcher::start_new(partition.clone(), tx.clone()); + + self.watchers.push(watcher); + } + + { + let vfs_session = self.vfs_session.clone(); + let rbx_session = self.rbx_session.clone(); + + thread::spawn(move || { + loop { + match rx.recv() { + Ok(change) => { + { + let mut vfs_session = vfs_session.write().unwrap(); + vfs_session.handle_change(&change); + } + + { + let mut rbx_session = rbx_session.write().unwrap(); + rbx_session.handle_change(&change); + } + }, + Err(_) => break, + } + } + }); + } + } + + pub fn stop(self) { + } + + pub fn get_vfs_session(&self) -> Arc> { + self.vfs_session.clone() + } + + pub fn get_rbx_session(&self) -> Arc> { + self.rbx_session.clone() + } + + pub fn get_message_session(&self) -> MessageSession { + self.message_session.clone() + } +} diff --git a/server/src/vfs/mod.rs b/server/src/vfs/mod.rs deleted file mode 100644 index a19dc4d0..00000000 --- a/server/src/vfs/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod vfs_session; -mod vfs_item; -mod vfs_watcher; - -pub use self::vfs_session::*; -pub use self::vfs_item::*; -pub use self::vfs_watcher::*; diff --git a/server/src/vfs/vfs_item.rs b/server/src/vfs/vfs_item.rs deleted file mode 100644 index 89fd2608..00000000 --- a/server/src/vfs/vfs_item.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::collections::HashMap; - -/// A VfsItem represents either a file or directory as it came from the filesystem. -/// -/// The interface here is intentionally simplified to make it easier to traverse -/// files that have been read into memory. -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", tag = "type")] -pub enum VfsItem { - File { - route: Vec, - file_name: String, - contents: String, - }, - Dir { - route: Vec, - file_name: String, - children: HashMap, - }, -} - -impl VfsItem { - pub fn name(&self) -> &String { - match self { - &VfsItem::File { ref file_name , .. } => file_name, - &VfsItem::Dir { ref file_name , .. } => file_name, - } - } - - pub fn route(&self) -> &[String] { - match self { - &VfsItem::File { ref route, .. } => route, - &VfsItem::Dir { ref route, .. } => route, - } - } -} diff --git a/server/src/vfs/vfs_session.rs b/server/src/vfs/vfs_session.rs deleted file mode 100644 index 52945d0d..00000000 --- a/server/src/vfs/vfs_session.rs +++ /dev/null @@ -1,220 +0,0 @@ -use std::collections::HashMap; -use std::fs::{self, File}; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::time::Instant; - -use plugin::PluginChain; -use vfs::VfsItem; - -/// Represents a virtual layer over multiple parts of the filesystem. -/// -/// Paths in this system are represented as slices of strings, and are always -/// relative to a partition, which is an absolute path into the real filesystem. -pub struct VfsSession { - /// Contains all of the partitions mounted by the Vfs. - /// - /// These must be absolute paths! - partitions: HashMap, - - /// A chronologically-sorted list of routes that changed since the Vfs was - /// created, along with a timestamp denoting when. - change_history: Vec, - - /// When the Vfs was initialized; used for change tracking. - start_time: Instant, - - plugin_chain: &'static PluginChain, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VfsChange { - timestamp: f64, - route: Vec, -} - -impl VfsSession { - pub fn new(plugin_chain: &'static PluginChain) -> VfsSession { - VfsSession { - partitions: HashMap::new(), - start_time: Instant::now(), - change_history: Vec::new(), - plugin_chain, - } - } - - pub fn get_partitions(&self) -> &HashMap { - &self.partitions - } - - pub fn insert_partition>(&mut self, name: &str, path: P) { - let path = path.into(); - - assert!(path.is_absolute()); - - self.partitions.insert(name.to_string(), path.into()); - } - - fn route_to_path(&self, route: &[String]) -> Option { - let (partition_name, rest) = match route.split_first() { - Some((first, rest)) => (first, rest), - None => return None, - }; - - let partition = match self.partitions.get(partition_name) { - Some(v) => v, - None => return None, - }; - - // It's possible that the partition points to a file if `rest` is empty. - // Joining "" onto a path will put a trailing slash on, which causes - // file reads to fail. - let full_path = if rest.is_empty() { - partition.clone() - } else { - let joined = rest.join("/"); - let relative = Path::new(&joined); - - partition.join(relative) - }; - - Some(full_path) - } - - fn read_dir>(&self, route: &[String], path: P) -> Result { - let path = path.as_ref(); - let reader = match fs::read_dir(path) { - Ok(v) => v, - Err(_) => return Err(()), - }; - - let mut children = HashMap::new(); - - for entry in reader { - let entry = match entry { - Ok(v) => v, - Err(_) => return Err(()), - }; - - let path = entry.path(); - let name = path.file_name().unwrap().to_string_lossy().into_owned(); - - let mut child_route = route.iter().cloned().collect::>(); - child_route.push(name.clone()); - - match self.read_path(&child_route, &path) { - Ok(child_item) => { - children.insert(name, child_item); - }, - Err(_) => {}, - } - } - - let file_name = path.file_name().unwrap().to_string_lossy().into_owned(); - - Ok(VfsItem::Dir { - route: route.iter().cloned().collect::>(), - file_name, - children, - }) - } - - fn read_file>(&self, route: &[String], path: P) -> Result { - let path = path.as_ref(); - let mut file = match File::open(path) { - Ok(v) => v, - Err(_) => return Err(()), - }; - - let mut contents = String::new(); - - match file.read_to_string(&mut contents) { - Ok(_) => {}, - Err(_) => return Err(()), - } - - let file_name = path.file_name().unwrap().to_string_lossy().into_owned(); - - Ok(VfsItem::File { - route: route.iter().cloned().collect::>(), - file_name, - contents, - }) - } - - fn read_path>(&self, route: &[String], path: P) -> Result { - let path = path.as_ref(); - - let metadata = match fs::metadata(path) { - Ok(v) => v, - Err(_) => return Err(()), - }; - - if metadata.is_dir() { - self.read_dir(route, path) - } else if metadata.is_file() { - self.read_file(route, path) - } else { - Err(()) - } - } - - /// Get the current time, used for logging timestamps for file changes. - pub fn current_time(&self) -> f64 { - let elapsed = self.start_time.elapsed(); - - elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9 - } - - /// Register a new change to the filesystem at the given timestamp and VFS - /// route. - pub fn add_change(&mut self, timestamp: f64, route: Vec) { - match self.plugin_chain.handle_file_change(&route) { - Some(routes) => { - for route in routes { - self.change_history.push(VfsChange { - timestamp, - route, - }); - } - }, - None => {} - } - } - - /// Collect a list of changes that occured since the given timestamp. - pub fn changes_since(&self, timestamp: f64) -> &[VfsChange] { - let mut marker: Option = None; - - for (index, value) in self.change_history.iter().enumerate().rev() { - if value.timestamp >= timestamp { - marker = Some(index); - } else { - break; - } - } - - if let Some(index) = marker { - &self.change_history[index..] - } else { - &self.change_history[..0] - } - } - - /// Read an item from the filesystem using the given VFS route. - pub fn read(&self, route: &[String]) -> Result { - match self.route_to_path(route) { - Some(path) => self.read_path(route, &path), - None => Err(()), - } - } - - pub fn write(&self, _route: &[String], _item: VfsItem) -> Result<(), ()> { - unimplemented!() - } - - pub fn delete(&self, _route: &[String]) -> Result<(), ()> { - unimplemented!() - } -} diff --git a/server/src/vfs/vfs_watcher.rs b/server/src/vfs/vfs_watcher.rs deleted file mode 100644 index 0e7ac27d..00000000 --- a/server/src/vfs/vfs_watcher.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::path::PathBuf; -use std::sync::{mpsc, Arc, Mutex}; -use std::thread; -use std::time::Duration; - -use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; - -use pathext::path_to_route; -use vfs::VfsSession; - -/// An object that registers watchers on the real filesystem and relays those -/// changes to the virtual filesystem layer. -pub struct VfsWatcher { - vfs: Arc>, -} - -impl VfsWatcher { - pub fn new(vfs: Arc>) -> VfsWatcher { - VfsWatcher { - vfs, - } - } - - fn start_watcher( - vfs: Arc>, - rx: mpsc::Receiver, - partition_name: String, - root_path: PathBuf, - ) { - loop { - let event = rx.recv().unwrap(); - - let mut vfs = vfs.lock().unwrap(); - let current_time = vfs.current_time(); - - match event { - DebouncedEvent::Write(ref change_path) | - DebouncedEvent::Create(ref change_path) | - DebouncedEvent::Remove(ref change_path) => { - if let Some(mut route) = path_to_route(&root_path, change_path) { - route.insert(0, partition_name.clone()); - - vfs.add_change(current_time, route); - } else { - eprintln!("Failed to get route from {}", change_path.display()); - } - }, - DebouncedEvent::Rename(ref from_change, ref to_change) => { - if let Some(mut route) = path_to_route(&root_path, from_change) { - route.insert(0, partition_name.clone()); - - vfs.add_change(current_time, route); - } else { - eprintln!("Failed to get route from {}", from_change.display()); - } - - if let Some(mut route) = path_to_route(&root_path, to_change) { - route.insert(0, partition_name.clone()); - - vfs.add_change(current_time, route); - } else { - eprintln!("Failed to get route from {}", to_change.display()); - } - }, - _ => {}, - } - } - } - - pub fn start(self) { - let mut watchers = Vec::new(); - - // Create an extra scope so that `vfs` gets dropped and unlocked - { - let vfs = self.vfs.lock().unwrap(); - - for (ref partition_name, ref root_path) in vfs.get_partitions() { - let (tx, rx) = mpsc::channel(); - - let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_millis(200)) - .expect("Unable to create watcher! This is a bug in Rojo."); - - match watcher.watch(&root_path, RecursiveMode::Recursive) { - Ok(_) => (), - Err(_) => { - panic!("Unable to watch partition {}, with path {}! Make sure that it's a file or directory.", partition_name, root_path.display()); - }, - } - - watchers.push(watcher); - - { - let partition_name = partition_name.to_string(); - let root_path = root_path.to_path_buf(); - let vfs = self.vfs.clone(); - - thread::spawn(move || { - Self::start_watcher(vfs, rx, partition_name, root_path); - }); - } - } - } - - loop { - thread::park(); - } - } -} diff --git a/server/src/vfs_session.rs b/server/src/vfs_session.rs new file mode 100644 index 00000000..c4dc3e0b --- /dev/null +++ b/server/src/vfs_session.rs @@ -0,0 +1,242 @@ +use std::collections::HashMap; +use std::io::Read; +use std::fs::{self, File}; +use std::mem; + +use file_route::FileRoute; +use project::Project; + +/// Represents a file or directory that has been read from the filesystem. +#[derive(Debug, Clone)] +pub enum FileItem { + File { + contents: String, + route: FileRoute, + }, + Directory { + children: HashMap, + route: FileRoute, + }, +} + +impl FileItem { + pub fn get_route(&self) -> &FileRoute { + match self { + FileItem::File { route, .. } => route, + FileItem::Directory { route, .. } => route, + } + } +} + +#[derive(Debug, Clone)] +pub enum FileChange { + Created(FileRoute), + Deleted(FileRoute), + Updated(FileRoute), + Moved(FileRoute, FileRoute), +} + +pub struct VfsSession { + pub project: Project, + + /// The in-memory files associated with each partition. + pub partition_files: HashMap, +} + +impl VfsSession { + pub fn new(project: Project) -> VfsSession { + VfsSession { + project, + partition_files: HashMap::new(), + } + } + + pub fn read_partitions(&mut self) { + for partition_name in self.project.partitions.keys() { + let route = FileRoute { + partition: partition_name.clone(), + route: Vec::new(), + }; + + let file_item = self.read(&route).expect("Couldn't load partitions"); + + self.partition_files.insert(partition_name.clone(), file_item); + } + } + + pub fn handle_change(&mut self, change: &FileChange) -> Option<()> { + match change { + FileChange::Created(route) | FileChange::Updated(route) => { + let new_item = self.read(&route).ok()?; + self.set_file_item(new_item); + }, + FileChange::Deleted(route) => { + self.delete_route(&route); + }, + FileChange::Moved(from_route, to_route) => { + let new_item = self.read(&to_route).ok()?; + self.delete_route(&from_route); + self.set_file_item(new_item); + }, + } + + None + } + + pub fn get_by_route(&self, route: &FileRoute) -> Option<&FileItem> { + let partition = self.partition_files.get(&route.partition)?; + let mut current = partition; + + for piece in &route.route { + match current { + FileItem::File { .. } => return None, + FileItem::Directory { children, .. } => { + current = children.get(piece)?; + }, + } + } + + Some(current) + } + + pub fn get_by_route_mut(&mut self, route: &FileRoute) -> Option<&mut FileItem> { + let mut current = self.partition_files.get_mut(&route.partition)?; + + for piece in &route.route { + let mut next = match { current } { + FileItem::File { .. } => return None, + FileItem::Directory { children, .. } => { + children.get_mut(piece)? + }, + }; + + current = next; + } + + Some(current) + } + + pub fn set_file_item(&mut self, item: FileItem) { + match self.get_by_route_mut(item.get_route()) { + Some(existing) => { + mem::replace(existing, item); + return; + }, + None => {}, + } + + if item.get_route().route.len() > 0 { + let mut parent_route = item.get_route().clone(); + let child_name = parent_route.route.pop().unwrap(); + + let mut parent_children = HashMap::new(); + parent_children.insert(child_name, item); + + let parent_item = FileItem::Directory { + route: parent_route, + children: parent_children, + }; + + self.set_file_item(parent_item); + } else { + self.partition_files.insert(item.get_route().partition.clone(), item); + } + } + + pub fn delete_route(&mut self, route: &FileRoute) -> Option<()> { + if route.route.len() == 0 { + self.partition_files.remove(&route.partition); + return Some(()); + } + + let mut current = self.partition_files.get_mut(&route.partition)?; + + for i in 0..(route.route.len() - 1) { + let piece = &route.route[i]; + + let mut next = match { current } { + FileItem::File { .. } => return None, + FileItem::Directory { children, .. } => { + children.get_mut(piece)? + }, + }; + + current = next; + } + + match current { + FileItem::Directory { children, .. } => { + children.remove(route.route.last().unwrap().as_str()); + }, + _ => {}, + } + + Some(()) + } + + fn read(&self, route: &FileRoute) -> Result { + let partition_path = &self.project.partitions.get(&route.partition) + .ok_or(())?.path; + let path = route.to_path_buf(partition_path); + + let metadata = fs::metadata(path) + .map_err(|_| ())?; + + if metadata.is_dir() { + self.read_directory(route) + } else if metadata.is_file() { + self.read_file(route) + } else { + Err(()) + } + } + + fn read_file(&self, route: &FileRoute) -> Result { + let partition_path = &self.project.partitions.get(&route.partition) + .ok_or(())?.path; + let path = route.to_path_buf(partition_path); + + let mut file = File::open(path) + .map_err(|_| ())?; + + let mut contents = String::new(); + + file.read_to_string(&mut contents) + .map_err(|_| ())?; + + Ok(FileItem::File { + contents, + route: route.clone(), + }) + } + + fn read_directory(&self, route: &FileRoute) -> Result { + let partition_path = &self.project.partitions.get(&route.partition) + .ok_or(())?.path; + let path = route.to_path_buf(partition_path); + + let reader = fs::read_dir(path) + .map_err(|_| ())?; + + let mut children = HashMap::new(); + + for entry in reader { + let entry = entry + .map_err(|_| ())?; + + let path = entry.path(); + let name = path.file_name().unwrap().to_string_lossy().into_owned(); + + let child_route = route.extended_with(&[&name]); + + let child_item = self.read(&child_route)?; + + children.insert(name, child_item); + } + + Ok(FileItem::Directory { + children, + route: route.clone(), + }) + } +} diff --git a/server/src/web.rs b/server/src/web.rs index dd05b2a4..7c81641a 100644 --- a/server/src/web.rs +++ b/server/src/web.rs @@ -1,225 +1,212 @@ -use std::io::Read; -use std::sync::{Arc, Mutex}; +use std::borrow::Cow; +use std::collections::HashMap; +use std::sync::{mpsc, RwLock, Arc}; -use rouille; -use serde; -use serde_json; +use rouille::{self, Request, Response}; +use id::Id; +use message_session::{MessageSession, Message}; use project::Project; -use vfs::{VfsSession, VfsChange}; use rbx::RbxInstance; -use plugin::PluginChain; - -static MAX_BODY_SIZE: usize = 25 * 1024 * 1024; // 25 MiB +use rbx_session::RbxSession; +use session::Session; /// The set of configuration the web server needs to start. pub struct WebConfig { pub port: u64, - pub verbose: bool, + pub project: Project, pub server_id: u64, + pub rbx_session: Arc>, + pub message_session: MessageSession, } -#[derive(Debug, Serialize)] +impl WebConfig { + pub fn from_session(server_id: u64, port: u64, session: &Session) -> WebConfig { + WebConfig { + port, + server_id, + project: session.project.clone(), + rbx_session: session.get_rbx_session(), + message_session: session.get_message_session(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -struct ServerInfo<'a> { +pub struct ServerInfoResponse<'a> { + pub server_id: &'a str, + pub server_version: &'a str, + pub protocol_version: u64, + pub partitions: HashMap>, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadAllResponse<'a> { + pub server_id: &'a str, + pub message_cursor: i32, + pub instances: Cow<'a, HashMap>, + pub partition_instances: Cow<'a, HashMap>, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadResponse<'a> { + pub server_id: &'a str, + pub message_cursor: i32, + pub instances: HashMap>, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeResponse<'a> { + pub server_id: &'a str, + pub message_cursor: i32, + pub messages: Cow<'a, [Message]>, +} + +pub struct Server { + config: WebConfig, server_version: &'static str, - protocol_version: u64, - server_id: &'a str, - project: &'a Project, - current_time: f64, + server_id: String, } -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct ReadResult<'a> { - items: Vec>, - server_id: &'a str, - current_time: f64, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct ChangesResult<'a> { - changes: &'a [VfsChange], - server_id: &'a str, - current_time: f64, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct WriteSpecifier { - route: String, - item: RbxInstance, -} - -fn json(value: T) -> rouille::Response { - let data = serde_json::to_string(&value).unwrap(); - rouille::Response::from_data("application/json", data) -} - -/// Pulls text that may be JSON out of a Rouille Request object. -/// -/// Doesn't do any actual parsing -- all this method does is verify the content -/// type of the request and read the request's body. -fn read_json_text(request: &rouille::Request) -> Option { - // Bail out if the request body isn't marked as JSON - match request.header("Content-Type") { - Some(header) => if !header.starts_with("application/json") { - return None; - }, - None => return None, +impl Server { + pub fn new(config: WebConfig) -> Server { + Server { + server_version: env!("CARGO_PKG_VERSION"), + server_id: config.server_id.to_string(), + config, + } } - let body = match request.data() { - Some(v) => v, - None => return None, - }; - - // Allocate a buffer and read up to MAX_BODY_SIZE+1 bytes into it. - let mut out = Vec::new(); - match body.take(MAX_BODY_SIZE.saturating_add(1) as u64).read_to_end(&mut out) { - Ok(_) => {}, - Err(_) => return None, - } - - // If the body was too big (MAX_BODY_SIZE+1), we abort instead of trying to - // process it. - if out.len() > MAX_BODY_SIZE { - return None; - } - - let parsed = match String::from_utf8(out) { - Ok(v) => v, - Err(_) => return None, - }; - - Some(parsed) -} - -/// Reads the body out of a Rouille Request and attempts to turn it into JSON. -fn read_json(request: &rouille::Request) -> Option -where - T: serde::de::DeserializeOwned, -{ - let body = match read_json_text(&request) { - Some(v) => v, - None => return None, - }; - - let parsed = match serde_json::from_str(&body) { - Ok(v) => v, - Err(_) => return None, - }; - - // TODO: Change return type to some sort of Result - - Some(parsed) -} - -/// Start the Rojo web server and park our current thread. -pub fn start(config: WebConfig, project: Project, plugin_chain: &'static PluginChain, vfs: Arc>) { - let address = format!("localhost:{}", config.port); - - let server_id = config.server_id.to_string(); - - rouille::start_server(address, move |request| { + pub fn handle_request(&self, request: &Request) -> Response { router!(request, (GET) (/) => { + Response::text("Rojo up and running!") + }, + + (GET) (/api/rojo) => { // Get a summary of information about the server. - let current_time = { - let vfs = vfs.lock().unwrap(); + let mut partitions = HashMap::new(); - vfs.current_time() - }; - - json(ServerInfo { - server_version: env!("CARGO_PKG_VERSION"), - protocol_version: 1, - server_id: &server_id, - project: &project, - current_time, - }) - }, - - (GET) (/changes/{ last_time: f64 }) => { - // Get the list of changes since the given time. - - let vfs = vfs.lock().unwrap(); - let current_time = vfs.current_time(); - let changes = vfs.changes_since(last_time); - - json(ChangesResult { - changes, - server_id: &server_id, - current_time, - }) - }, - - (POST) (/read) => { - // Read some instances from the server according to a JSON - // format body. - - let read_request: Vec> = match read_json(&request) { - Some(v) => v, - None => return rouille::Response::empty_400(), - }; - - // Read the files off of the filesystem that the client - // requested. - let (items, current_time) = { - let vfs = vfs.lock().unwrap(); - - let current_time = vfs.current_time(); - - let mut items = Vec::new(); - - for route in &read_request { - match vfs.read(&route) { - Ok(v) => items.push(Some(v)), - Err(_) => items.push(None), - } - } - - (items, current_time) - }; - - // Transform all of our VfsItem objects into Roblox instances - // the client can use. - let rbx_items = items - .iter() - .map(|item| { - match *item { - Some(ref item) => plugin_chain.transform_file(item), - None => None, - } - }) - .collect::>(); - - if config.verbose { - println!("Got read request: {:?}", read_request); - println!("Responding with:\n\t{:?}", rbx_items); + for partition in self.config.project.partitions.values() { + partitions.insert(partition.name.clone(), partition.target.clone()); } - json(ReadResult { - server_id: &server_id, - items: rbx_items, - current_time, + Response::json(&ServerInfoResponse { + server_version: self.server_version, + protocol_version: 2, + server_id: &self.server_id, + partitions: partitions, }) }, - (POST) (/write) => { - // Not yet implemented. + (GET) (/api/subscribe/{ cursor: i32 }) => { + // Retrieve any messages past the given cursor index, and if + // there weren't any, subscribe to receive any new messages. - let _write_request: Vec = match read_json(&request) { - Some(v) => v, - None => return rouille::Response::empty_400(), - }; + // Did the client miss any messages since the last subscribe? + { + let messages = self.config.message_session.messages.read().unwrap(); - rouille::Response::empty_404() + if cursor > messages.len() as i32 { + return Response::json(&SubscribeResponse { + server_id: &self.server_id, + messages: Cow::Borrowed(&[]), + message_cursor: messages.len() as i32 - 1, + }); + } + + if cursor < messages.len() as i32 - 1 { + let new_messages = &messages[(cursor + 1) as usize..]; + let new_cursor = cursor + new_messages.len() as i32; + + return Response::json(&SubscribeResponse { + server_id: &self.server_id, + messages: Cow::Borrowed(new_messages), + message_cursor: new_cursor, + }); + } + } + + let (tx, rx) = mpsc::channel(); + + let sender_id = self.config.message_session.subscribe(tx); + + match rx.recv() { + Ok(_) => (), + Err(_) => return Response::text("error!").with_status_code(500), + } + + self.config.message_session.unsubscribe(sender_id); + + { + let messages = self.config.message_session.messages.read().unwrap(); + let new_messages = &messages[(cursor + 1) as usize..]; + let new_cursor = cursor + new_messages.len() as i32; + + Response::json(&SubscribeResponse { + server_id: &self.server_id, + messages: Cow::Borrowed(new_messages), + message_cursor: new_cursor, + }) + } }, - _ => rouille::Response::empty_404() + (GET) (/api/read_all) => { + let rbx_session = self.config.rbx_session.read().unwrap(); + + let message_cursor = self.config.message_session.get_message_cursor(); + + Response::json(&ReadAllResponse { + server_id: &self.server_id, + message_cursor, + instances: Cow::Borrowed(rbx_session.tree.get_all_instances()), + partition_instances: Cow::Borrowed(&rbx_session.partition_instances), + }) + }, + + (GET) (/api/read/{ id_list: String }) => { + let requested_ids = id_list + .split(",") + .map(str::parse::) + .collect::, _>>(); + + let requested_ids = match requested_ids { + Ok(v) => v, + Err(_) => return rouille::Response::text("Malformed ID list").with_status_code(400), + }; + + let rbx_session = self.config.rbx_session.read().unwrap(); + + let message_cursor = self.config.message_session.get_message_cursor(); + + let mut instances = HashMap::new(); + + for requested_id in &requested_ids { + rbx_session.tree.get_instance(*requested_id, &mut instances); + } + + Response::json(&ReadResponse { + server_id: &self.server_id, + message_cursor, + instances, + }) + }, + + _ => Response::empty_404() ) - }); + } +} + +/// Start the Rojo web server, taking over the current thread. +#[allow(unreachable_code)] +pub fn start(config: WebConfig) { + let address = format!("localhost:{}", config.port); + let server = Server::new(config); + + rouille::start_server(address, move |request| server.handle_request(request)); } diff --git a/server/src/web_util.rs b/server/src/web_util.rs new file mode 100644 index 00000000..0c344e6c --- /dev/null +++ b/server/src/web_util.rs @@ -0,0 +1,43 @@ +use std::io::Read; + +use rouille; +use serde; +use serde_json; + +static MAX_BODY_SIZE: usize = 100 * 1024 * 1024; // 100 MiB + +/// Pulls text that may be JSON out of a Rouille Request object. +/// +/// Doesn't do any actual parsing -- all this method does is verify the content +/// type of the request and read the request's body. +fn read_json_text(request: &rouille::Request) -> Option { + // Bail out if the request body isn't marked as JSON + let content_type = request.header("Content-Type")?; + + if !content_type.starts_with("application/json") { + return None; + } + + let body = request.data()?; + + // Allocate a buffer and read up to MAX_BODY_SIZE+1 bytes into it. + let mut out = Vec::new(); + body.take(MAX_BODY_SIZE.saturating_add(1) as u64).read_to_end(&mut out).ok()?; + + // If the body was too big (MAX_BODY_SIZE+1), we abort instead of trying to + // process it. + if out.len() > MAX_BODY_SIZE { + return None; + } + + String::from_utf8(out).ok() +} + +/// Reads the body out of a Rouille Request and attempts to turn it into JSON. +pub fn read_json(request: &rouille::Request) -> Option +where + T: serde::de::DeserializeOwned, +{ + let body = read_json_text(&request)?; + serde_json::from_str(&body).ok()? +} diff --git a/server/test-projects/empty/rojo.json b/server/test-projects/empty/rojo.json new file mode 100644 index 00000000..51c7917c --- /dev/null +++ b/server/test-projects/empty/rojo.json @@ -0,0 +1,5 @@ +{ + "name": "empty", + "servePort": 23456, + "partitions": {} +} \ No newline at end of file diff --git a/server/test-projects/one-partition/lib/a.lua b/server/test-projects/one-partition/lib/a.lua new file mode 100644 index 00000000..f23fd06a --- /dev/null +++ b/server/test-projects/one-partition/lib/a.lua @@ -0,0 +1 @@ +-- a.lua \ No newline at end of file diff --git a/server/test-projects/one-partition/lib/a.server.lua b/server/test-projects/one-partition/lib/a.server.lua new file mode 100644 index 00000000..08491e94 --- /dev/null +++ b/server/test-projects/one-partition/lib/a.server.lua @@ -0,0 +1 @@ +-- a.server.lua \ No newline at end of file diff --git a/server/test-projects/one-partition/lib/b.client.lua b/server/test-projects/one-partition/lib/b.client.lua new file mode 100644 index 00000000..12d034fa --- /dev/null +++ b/server/test-projects/one-partition/lib/b.client.lua @@ -0,0 +1 @@ +-- b.client.lua \ No newline at end of file diff --git a/server/test-projects/one-partition/rojo.json b/server/test-projects/one-partition/rojo.json new file mode 100644 index 00000000..d774a8f8 --- /dev/null +++ b/server/test-projects/one-partition/rojo.json @@ -0,0 +1,10 @@ +{ + "name": "one-partition", + "servePort": 23456, + "partitions": { + "lib": { + "path": "lib", + "target": "ReplicatedStorage.OnePartition" + } + } +} \ No newline at end of file diff --git a/server/tests/test_util/mod.rs b/server/tests/test_util/mod.rs new file mode 100644 index 00000000..cca6744b --- /dev/null +++ b/server/tests/test_util/mod.rs @@ -0,0 +1,22 @@ +use rouille::Request; + +use librojo::web::Server; + +pub trait HttpTestUtil { + fn get_string(&self, url: &str) -> String; +} + +impl HttpTestUtil for Server { + fn get_string(&self, url: &str) -> String { + let info_request = Request::fake_http("GET", url, vec![], vec![]); + let response = self.handle_request(&info_request); + + assert_eq!(response.status_code, 200); + + let (mut reader, _) = response.data.into_reader_and_size(); + let mut body = String::new(); + reader.read_to_string(&mut body).unwrap(); + + body + } +} diff --git a/server/tests/web.rs b/server/tests/web.rs new file mode 100644 index 00000000..d59e822e --- /dev/null +++ b/server/tests/web.rs @@ -0,0 +1,170 @@ +extern crate rouille; +extern crate serde_json; +extern crate serde; + +extern crate librojo; + +mod test_util; +use test_util::*; + +use std::collections::HashMap; +use std::path::PathBuf; + +use librojo::{ + session::Session, + project::Project, + web::{Server, WebConfig, ServerInfoResponse, ReadResponse, ReadAllResponse}, +}; + +#[test] +fn empty() { + let project_path = { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test-projects/empty"); + path + }; + + let project = Project::load(&project_path).unwrap(); + let mut session = Session::new(project.clone()); + session.start(); + + let web_config = WebConfig::from_session(0, project.serve_port, &session); + let server = Server::new(web_config); + + { + let body = server.get_string("/api/rojo"); + let response = serde_json::from_str::(&body).unwrap(); + + assert_eq!(response.server_id, "0"); + assert_eq!(response.protocol_version, 2); + assert_eq!(response.partitions.len(), 0); + } + + { + let body = server.get_string("/api/read_all"); + let response = serde_json::from_str::(&body).unwrap(); + + assert_eq!(response.server_id, "0"); + assert_eq!(response.message_cursor, -1); + assert_eq!(response.instances.len(), 0); + } + + { + let body = server.get_string("/api/read/0"); + let response = serde_json::from_str::(&body).unwrap(); + + assert_eq!(response.server_id, "0"); + assert_eq!(response.message_cursor, -1); + assert_eq!(response.instances.len(), 0); + } +} + +#[test] +fn one_partition() { + let project_path = { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test-projects/one-partition"); + path + }; + + let project = Project::load(&project_path).unwrap(); + let mut session = Session::new(project.clone()); + session.start(); + + let web_config = WebConfig::from_session(0, project.serve_port, &session); + let server = Server::new(web_config); + + { + let body = server.get_string("/api/rojo"); + let response = serde_json::from_str::(&body).unwrap(); + + let mut partitions = HashMap::new(); + partitions.insert("lib".to_string(), vec!["ReplicatedStorage".to_string(), "OnePartition".to_string()]); + + assert_eq!(response.server_id, "0"); + assert_eq!(response.protocol_version, 2); + assert_eq!(response.partitions, partitions); + } + + { + let body = server.get_string("/api/read_all"); + let response = serde_json::from_str::(&body).unwrap(); + + let partition_id = *response.partition_instances.get("lib").unwrap(); + + assert_eq!(response.server_id, "0"); + assert_eq!(response.message_cursor, -1); + assert_eq!(response.instances.len(), 4); // root and three children + + let mut found_root = false; + let mut found_module = false; + let mut found_client = false; + let mut found_server = false; + + for (id, instance) in response.instances.iter() { + match instance.class_name.as_str() { + // TOOD: Should partition roots (and other directories) be some + // magical object instead of Folder? + "Folder" => { + assert!(!found_root); + found_root = true; + + assert_eq!(*id, partition_id); + + // TODO: Should this actually equal the last part of the + // partition's target? + assert_eq!(instance.name, "OnePartition"); + + assert_eq!(instance.properties.len(), 0); + assert_eq!(instance.parent, None); + assert_eq!(instance.children.len(), 3); + }, + "ModuleScript" => { + assert!(!found_module); + found_module = true; + + let mut properties = HashMap::new(); + properties.insert("Source".to_string(), "-- a.lua".to_string()); + + assert_eq!(instance.name, "a"); + assert_eq!(instance.properties, properties); + assert_eq!(instance.parent, Some(partition_id)); + assert_eq!(instance.children.len(), 0); + }, + "LocalScript" => { + assert!(!found_client); + found_client = true; + + let mut properties = HashMap::new(); + properties.insert("Source".to_string(), "-- b.client.lua".to_string()); + + assert_eq!(instance.name, "b"); + assert_eq!(instance.properties, properties); + assert_eq!(instance.parent, Some(partition_id)); + assert_eq!(instance.children.len(), 0); + }, + "Script" => { + assert!(!found_server); + found_server = true; + + let mut properties = HashMap::new(); + properties.insert("Source".to_string(), "-- a.server.lua".to_string()); + + assert_eq!(instance.name, "a"); + assert_eq!(instance.properties, properties); + assert_eq!(instance.parent, Some(partition_id)); + assert_eq!(instance.children.len(), 0); + }, + _ => panic!("Unexpected instance!"), + } + } + + assert!(found_root); + assert!(found_module); + assert!(found_client); + assert!(found_server); + } + + // TODO: Test /read + // TODO: Test /subscribe +} \ No newline at end of file