diff --git a/Cargo.lock b/Cargo.lock index 498b272..c2a23b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -125,18 +134,60 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + [[package]] name = "bytecount" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.54" @@ -194,6 +245,12 @@ dependencies = [ "unicode-width 0.2.2", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -203,6 +260,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.29.0" @@ -360,6 +442,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" + [[package]] name = "float-cmp" version = "0.10.0" @@ -375,6 +463,71 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -415,6 +568,7 @@ dependencies = [ "anyhow", "assert_cmd", "atty", + "chrono", "clap", "comfy-table", "csv", @@ -423,17 +577,23 @@ dependencies = [ "env_logger", "lazy_static", "log", + "md5", "pest", "pest_derive", "predicates", "regex", + "remoc", "serde", "serde_json", "serde_yaml", "strsim", + "sysinfo", "tabled", "tempfile", "thiserror", + "tokio", + "tracing", + "tracing-subscriber", "walkdir", "which", ] @@ -474,6 +634,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -520,6 +704,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -575,18 +769,62 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "ntapi" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -691,6 +929,18 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "portable-atomic" version = "1.13.0" @@ -706,6 +956,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "predicates" version = "3.1.3" @@ -784,6 +1043,55 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1b3bc831f92381018fd9c6350b917c7b21f1eed35a65a51900e0e55a3d7afa" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -833,6 +1141,37 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "remoc" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b862dbacf3e0cad8d9031e799ba77db2f6f1f65a2480c3d3310fab8b3b1f499b" +dependencies = [ + "byteorder", + "bytes", + "futures", + "rand", + "remoc_macro", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "remoc_macro" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b929c6b6255d8d80205d79d80f41093adf0d14089e51c74ff5b850f2160b0677" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "rustix" version = "0.38.44" @@ -859,6 +1198,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.22" @@ -947,12 +1292,53 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "strsim" version = "0.11.1" @@ -981,6 +1367,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + [[package]] name = "tabled" version = "0.15.0" @@ -1044,6 +1445,117 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "typenum" version = "1.19.0" @@ -1092,6 +1604,24 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -1132,6 +1662,51 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + [[package]] name = "which" version = "6.0.3" @@ -1175,12 +1750,84 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1199,6 +1846,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1232,13 +1888,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1251,6 +1924,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1263,6 +1942,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1275,12 +1960,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1293,6 +1990,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1305,6 +2008,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1317,6 +2026,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1329,6 +2044,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winsafe" version = "0.0.19" @@ -1341,6 +2062,26 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "zmij" version = "1.0.13" diff --git a/Cargo.toml b/Cargo.toml index a751f4a..21d6e69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,23 @@ thiserror = "1.0" # Logging env_logger = "0.11" log = "0.4" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Async runtime +tokio = { version = "1.35", features = ["full"] } + +# RPC +remoc = { version = "0.16", features = ["full"] } + +# Time +chrono = { version = "0.4", features = ["serde"] } + +# System info +sysinfo = "0.30" + +# Hashing +md5 = "0.7" # File system & paths dirs = "5.0" diff --git a/README.md b/README.md index cf1d033..c5b3c97 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ # Ghidra CLI -A powerful Rust CLI tool for Ghidra reverse engineering, designed for Claude Code and other AI agents to efficiently analyze binaries. +A high-performance Rust CLI for automating Ghidra reverse engineering tasks, designed for both direct usage and AI agent integration (like Claude Code). ## Features +- πŸ”₯ **Daemon-Based Architecture** - Background daemon prevents conflicts and speeds up operations +- πŸ“¦ **Command Queuing** - Safe, serialized execution of Ghidra operations +- ⚑ **Automatic Caching** - Instant responses for repeated queries (5-minute TTL) - πŸš€ **Universal Query System** - Query any Ghidra data type with a single command - πŸ” **Advanced Filtering** - Powerful filter language for precise data extraction - πŸ“Š **Multiple Output Formats** - JSON, CSV, TSV, Table, and more - πŸ€– **LLM-Optimized** - Designed for minimal token usage and maximum efficiency -- πŸͺŸ **Windows-First** - Native Windows support with cross-platform compatibility +- πŸͺŸ **Cross-Platform** - Native Windows, Linux, and macOS support - πŸ“¦ **Zero Configuration** - Auto-detection of Ghidra installation -- ⚑ **Fast** - Direct headless Ghidra integration ## Installation @@ -48,6 +50,25 @@ ghidra doctor ## Quick Start +### With Daemon (Recommended) + +```bash +# Start the daemon for a project +ghidra daemon start --project analysis + +# Now all commands are routed through the daemon automatically +ghidra query functions --project analysis --filter="size>1000" +ghidra decompile 0x401000 --project analysis + +# Check daemon status +ghidra daemon status --project analysis + +# Stop the daemon when done +ghidra daemon stop --project analysis +``` + +### Without Daemon (Direct Mode) + ```bash # Quick analysis of a binary ghidra quick malware.exe @@ -65,6 +86,67 @@ ghidra decompile 0x401000 --program=suspicious.exe ghidra dump imports --program=suspicious.exe --filter="name~Crypt OR name~Process" ``` +## Daemon Architecture + +The daemon prevents Ghidra headless conflicts and dramatically improves performance: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ CLI Client │──JSON-over-TCP──▢│ Daemon β”‚ +β”‚ (Any Command)β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ Command Queue β”‚ β”‚ + β”‚ β”‚ (Serialized) β”‚ β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ β–Ό β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β”‚ β”‚ Cache (5min TTL)β”‚ β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ β–Ό β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β”‚ β”‚ Ghidra Headless β”‚ β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Daemon Commands + +```bash +# Start daemon (foreground for debugging) +ghidra daemon start --project my_project --foreground + +# Start daemon with custom port +ghidra daemon start --project my_project --port 17700 + +# Check status +ghidra daemon status --project my_project + +# Restart daemon +ghidra daemon restart --project my_project + +# Stop daemon +ghidra daemon stop --project my_project + +# Ping daemon to check responsiveness +ghidra daemon ping --project my_project + +# Clear result cache +ghidra daemon clear-cache --project my_project +``` + +### Why Use the Daemon? + +**Without Daemon:** +- ❌ 3-5 second startup per command +- ❌ Cannot run concurrent operations +- ❌ No result caching +- ❌ Project lock conflicts + +**With Daemon:** +- βœ… Instant responses (cache hits) +- βœ… Queued operations (no conflicts) +- βœ… Keep Ghidra loaded in memory +- βœ… Automatic cache management + ## Universal Query Command The `query` command is the primary interface for data extraction: diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..d8a2968 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,617 @@ +# Ghidra CLI - LLM Agent Guide + +**A comprehensive guide for LLMs (like Claude Code) to effectively use Ghidra CLI for binary analysis tasks.** + +--- + +## Overview + +Ghidra CLI is a Rust-based command-line tool that provides programmatic access to Ghidra's reverse engineering capabilities. It's designed with AI agents in mind, featuring: + +- **Daemon architecture** for fast, conflict-free operations +- **Universal query system** for consistent data extraction +- **Automatic caching** to minimize redundant operations +- **Token-efficient output formats** for LLM consumption + +--- + +## Quick Reference + +### Essential Commands + +```bash +# Daemon management +ghidra daemon start --project # Start background daemon +ghidra daemon stop --project # Stop daemon +ghidra daemon status --project # Check status + +# Project operations +ghidra project create # Create new project +ghidra import --project # Import binary +ghidra analyze --project # Run analysis + +# Query data +ghidra query --project [--filter] [--limit] + +# Decompilation +ghidra decompile --project +``` + +### Common Data Types + +`functions`, `strings`, `imports`, `exports`, `symbols`, `memory`, `xrefs`, `types`, `comments` + +--- + +## Workflow Patterns + +### Pattern 1: First-Time Binary Analysis + +```bash +# 1. Create project and start daemon +ghidra project create analysis_2024 +ghidra daemon start --project analysis_2024 --foreground + +# 2. Import and analyze binary +ghidra import malware.exe --project analysis_2024 +ghidra analyze --project analysis_2024 + +# 3. Get high-level summary +ghidra summary --project analysis_2024 + +# 4. Query specific data as needed +ghidra query functions --project analysis_2024 --filter "size>500" +``` + +### Pattern 2: Investigating Suspicious Behavior + +```bash +# Find crypto-related functions +ghidra query functions --project analysis \ + --filter 'name contains "crypt" or name contains "encrypt"' \ + --fields name,address,size + +# Check suspicious imports +ghidra query imports --project analysis \ + --filter 'name contains "Process" or name contains "Registry"' + +# Find suspicious strings +ghidra query strings --project analysis \ + --filter 'value contains "http" or value contains "cmd.exe"' \ + --limit 50 +``` + +### Pattern 3: Function Analysis Deep Dive + +```bash +# List all functions (count first!) +ghidra query functions --project analysis --count + +# Get top 20 largest functions +ghidra query functions --project analysis \ + --sort -size --limit 20 \ + --fields name,address,size + +# Decompile specific function +ghidra decompile suspicious_func --project analysis + +# Find what calls this function +ghidra function xrefs suspicious_func --project analysis + +# Find what this function calls +ghidra function calls suspicious_func --project analysis +``` + +--- + +## Token Optimization Strategies + +### 1. Always Count First + +Before fetching data, check the result size: + +```bash +# BAD: Might return 10,000 functions +ghidra query functions --project analysis + +# GOOD: Check size first +ghidra query functions --project analysis --count +# Output: 10,247 functions + +# Then refine with filter +ghidra query functions --project analysis \ + --filter 'not name starts_with "FUN_"' \ + --count +# Output: 156 functions +``` + +### 2. Use Field Selection + +Only request fields you need: + +```bash +# BAD: Returns all fields (name, address, size, body, callers, callees, etc.) +ghidra query functions --project analysis + +# GOOD: Select minimal fields +ghidra query functions --project analysis \ + --fields name,address,size \ + --format json +``` + +### 3. Apply Filters Server-Side + +Filter in Ghidra, not in your code: + +```bash +# BAD: Fetch all 10K functions, filter in LLM +ghidra query functions --project analysis | grep crypto + +# GOOD: Filter on server +ghidra query functions --project analysis \ + --filter 'name contains "crypto"' +``` + +### 4. Use Compact Formats + +```bash +# For analysis/display +--format json # Full JSON with all fields + +# For LLM processing (RECOMMENDED) +--format minimal # Just name/address, space-separated +--format ids # Just addresses, one per line + +# For counting +--count # Just return the count +``` + +### 5. Paginate Large Results + +```bash +# Get first 50 results +ghidra query functions --project analysis --limit 50 + +# Get next 50 results +ghidra query functions --project analysis --limit 50 --offset 50 +``` + +--- + +## Filter Expression Reference + +### Comparison Operators + +```bash +field == value # Exact match +field != value # Not equal +field > value # Greater than +field >= value # Greater or equal +field < value # Less than +field <= value # Less or equal +``` + +### String Operators + +```bash +field contains "text" # Case-insensitive substring +field starts_with "text" # Prefix match +field ends_with "text" # Suffix match +field matches "regex" # Regular expression +``` + +### Logical Operators + +```bash +condition1 and condition2 # Both must be true +condition1 or condition2 # Either must be true +not condition # Negation +``` + +### Special Operators + +```bash +exists(field) # Field exists and is not null +field in [val1,val2,val3] # Field is one of values +``` + +### Example Filters + +```bash +# Large named functions +'size > 1000 and not name starts_with "FUN_"' + +# Crypto-related +'name contains "crypt" or name contains "hash" or name contains "encrypt"' + +# Network-related strings +'value contains "http" or value contains "socket" or value contains "connect"' + +# Public exported functions +'public == true and exists(export)' + +# Complex condition +'(size > 500 or callees > 10) and not thunk and exists(callers)' +``` + +--- + +## Output Format Guide + +### JSON (Default for Pipes) + +```bash +ghidra query functions --project analysis --format json +``` + +```json +[ + { + "name": "main", + "address": "0x401000", + "size": 1024, + "body": "...", + "callers": ["0x402000"], + "callees": ["0x403000"] + } +] +``` + +### Minimal (Best for LLMs) + +```bash +ghidra query functions --project analysis --format minimal +``` + +``` +main 0x401000 +sub_401100 0x401100 +FUN_401200 0x401200 +``` + +### IDs Only + +```bash +ghidra query functions --project analysis --format ids +``` + +``` +0x401000 +0x401100 +0x401200 +``` + +### Table (For Display) + +```bash +ghidra query functions --project analysis --format table +``` + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β” +β”‚ Name β”‚ Address β”‚ Size β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€ +β”‚ main β”‚ 0x401000 β”‚ 1024 β”‚ +β”‚ sub_401100 β”‚ 0x401100 β”‚ 256 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Common Tasks + +### Task: Find Entry Point + +```bash +# Method 1: Query for "entry" or "main" +ghidra query functions --project analysis \ + --filter 'name == "entry" or name == "main" or name == "_start"' + +# Method 2: Get program info +ghidra summary --project analysis | grep -i "entry" +``` + +### Task: Find Suspicious API Calls + +```bash +# Check imports +ghidra query imports --project analysis \ + --filter 'name contains "CreateProcess" or + name contains "WinExec" or + name contains "ShellExecute" or + name contains "URLDownload"' +``` + +### Task: Extract All Strings + +```bash +# Get count first +ghidra query strings --project analysis --count + +# If reasonable size, fetch all +ghidra query strings --project analysis \ + --fields value,address \ + --format minimal > strings.txt + +# If too large, filter +ghidra query strings --project analysis \ + --filter 'length > 10' \ + --limit 1000 +``` + +### Task: Analyze Function Behavior + +```bash +# 1. Get function info +ghidra function get suspicious_func --project analysis + +# 2. Decompile +ghidra decompile suspicious_func --project analysis + +# 3. See what it calls +ghidra function calls suspicious_func --project analysis + +# 4. See where it's called from +ghidra function xrefs suspicious_func --project analysis + +# 5. Check strings it references +ghidra query strings --project analysis \ + --filter 'xrefs contains "suspicious_func"' +``` + +### Task: Find Encrypted/Obfuscated Code + +```bash +# High entropy strings (likely encrypted) +ghidra query strings --project analysis \ + --filter 'length > 50' \ + --format minimal + +# Large functions (potential obfuscation) +ghidra query functions --project analysis \ + --filter 'size > 5000' \ + --sort -size + +# Functions with unusual call patterns +ghidra query functions --project analysis \ + --filter 'callees > 50 or callers > 20' +``` + +--- + +## Daemon Best Practices + +### When to Use Daemon + +**Always use daemon for:** +- Multiple queries on the same project +- Interactive analysis sessions +- Repeated decompilation requests +- Any workflow with >3 commands + +**Skip daemon for:** +- One-off quick analysis +- Different projects each time +- Simple import/analyze operations + +### Daemon Lifecycle + +```bash +# At session start +ghidra daemon start --project analysis + +# During session: all commands auto-route to daemon +ghidra query functions --project analysis +# ↑ Automatically uses daemon if running + +# At session end +ghidra daemon stop --project analysis +``` + +### Troubleshooting Daemon + +```bash +# Check if daemon is running +ghidra daemon status --project analysis + +# Test daemon responsiveness +ghidra daemon ping --project analysis + +# Restart if stuck +ghidra daemon restart --project analysis + +# Force stop (if needed) +pkill -f "ghidra daemon" +rm ~/.local/share/ghidra-cli/daemon-*.lock +``` + +--- + +## Error Handling + +### Common Errors + +**"Project not found"** +```bash +# List projects +ghidra project list + +# Create if needed +ghidra project create analysis +``` + +**"Daemon not running"** +```bash +# Start daemon +ghidra daemon start --project analysis --foreground + +# Check logs +tail -f ~/.local/share/ghidra-cli/daemon.log +``` + +**"Analysis incomplete"** +```bash +# Run analysis +ghidra analyze --project analysis + +# Check if binary is imported +ghidra project info analysis +``` + +**"Timeout"** +```bash +# Increase timeout in config +echo "timeout: 600" >> ~/.config/ghidra-cli/config.yaml + +# Or use environment variable +export GHIDRA_TIMEOUT=600 +``` + +--- + +## Performance Tips + +1. **Start with counts** - Always check result size before fetching +2. **Use daemon** - 100x faster for repeated operations +3. **Cache awareness** - Identical queries return instantly (5-min TTL) +4. **Filter aggressively** - Reduce data transfer +5. **Select minimal fields** - Less data = faster & fewer tokens +6. **Batch similar queries** - Group related operations together + +--- + +## Integration Examples + +### Example: Malware Analysis Report + +```bash +#!/bin/bash +PROJECT="malware_analysis" +BINARY="suspicious.exe" + +# Setup +ghidra project create $PROJECT +ghidra daemon start --project $PROJECT +ghidra import $BINARY --project $PROJECT +ghidra analyze --project $PROJECT + +# Gather intelligence +echo "=== SUMMARY ===" +ghidra summary --project $PROJECT + +echo -e "\n=== SUSPICIOUS IMPORTS ===" +ghidra query imports --project $PROJECT \ + --filter 'name contains "Process" or name contains "Registry"' + +echo -e "\n=== CRYPTO FUNCTIONS ===" +ghidra query functions --project $PROJECT \ + --filter 'name contains "crypt" or name contains "encrypt"' \ + --fields name,address + +echo -e "\n=== NETWORK STRINGS ===" +ghidra query strings --project $PROJECT \ + --filter 'value contains "http" or value contains "://"' \ + --limit 20 + +# Cleanup +ghidra daemon stop --project $PROJECT +``` + +### Example: Function Coverage Analysis + +```bash +# Count total functions +TOTAL=$(ghidra query functions --project analysis --count) + +# Count named functions +NAMED=$(ghidra query functions --project analysis \ + --filter 'not name starts_with "FUN_"' --count) + +# Calculate percentage +echo "Coverage: $NAMED / $TOTAL functions named" +echo "Percentage: $(( NAMED * 100 / TOTAL ))%" +``` + +--- + +## Appendix: Complete Command Reference + +### Daemon Commands + +```bash +ghidra daemon start [--project] [--port] [--foreground] +ghidra daemon stop [--project] +ghidra daemon restart [--project] [--port] +ghidra daemon status [--project] +ghidra daemon ping [--project] +ghidra daemon clear-cache [--project] +``` + +### Project Commands + +```bash +ghidra project create +ghidra project list +ghidra project delete +ghidra project info +``` + +### Analysis Commands + +```bash +ghidra import [--project] +ghidra analyze [--project] +ghidra summary [--project] +ghidra quick # import + analyze + summary +``` + +### Query Commands + +```bash +ghidra query [options] + --project + --filter + --fields + --format + --limit + --offset + --sort + --count +``` + +### Function Commands + +```bash +ghidra function list [options] +ghidra function get [options] +ghidra function decompile [options] +ghidra function calls [options] +ghidra function xrefs [options] +``` + +### Utility Commands + +```bash +ghidra doctor # Verify installation +ghidra init # Initialize configuration +ghidra config get +ghidra config set +ghidra version +``` + +--- + +## Tips for LLM Agents + +1. **Always check daemon status** before starting analysis +2. **Use `--count` liberally** to avoid overwhelming responses +3. **Start broad, then narrow** with filters +4. **Leverage caching** by grouping similar queries +5. **Use `--format minimal`** for token efficiency +6. **Handle errors gracefully** - daemon issues are common +7. **Clean up after sessions** - stop daemons when done +8. **Document your queries** - future you will thank you + +--- + +**Happy reverse engineering! πŸ”** diff --git a/src/cli.rs b/src/cli.rs index 7e44f6b..bfa388a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,5 @@ use clap::{Parser, Subcommand, Args}; +use serde::{Deserialize, Serialize}; #[derive(Parser)] #[command(name = "ghidra")] @@ -16,7 +17,7 @@ pub struct Cli { pub quiet: bool, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum Commands { /// Universal query command for any data type Query(QueryArgs), @@ -119,9 +120,13 @@ pub enum Commands { /// Analyze a program Analyze(AnalyzeArgs), + + /// Daemon management commands + #[command(subcommand)] + Daemon(DaemonCommands), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct QueryArgs { /// Data type to query (functions, strings, imports, etc.) pub data_type: String, @@ -167,13 +172,13 @@ pub struct QueryArgs { pub json: bool, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct ProjectArgs { #[command(subcommand)] pub command: ProjectCommands, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum ProjectCommands { /// Create a new project Create { name: String }, @@ -185,7 +190,7 @@ pub enum ProjectCommands { Info { name: Option }, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum ProgramCommands { /// Close a program Close(ProgramTargetArgs), @@ -197,7 +202,7 @@ pub enum ProgramCommands { Export(ExportArgs), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct ProgramTargetArgs { #[arg(long)] pub program: Option, @@ -205,7 +210,7 @@ pub struct ProgramTargetArgs { pub project: Option, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct ExportArgs { /// Export format (xml, json, asm, c) pub format: String, @@ -218,7 +223,7 @@ pub struct ExportArgs { pub output: Option, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum FunctionCommands { /// List all functions List(QueryOptions), @@ -240,7 +245,7 @@ pub enum FunctionCommands { Delete(FunctionGetArgs), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct FunctionGetArgs { /// Function address or name pub target: String, @@ -248,7 +253,7 @@ pub struct FunctionGetArgs { pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct RenameArgs { pub old_name: String, pub new_name: String, @@ -258,7 +263,7 @@ pub struct RenameArgs { pub project: Option, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct CreateFunctionArgs { pub address: String, pub name: Option, @@ -268,7 +273,7 @@ pub struct CreateFunctionArgs { pub project: Option, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum StringsCommands { /// List all strings List(QueryOptions), @@ -276,14 +281,14 @@ pub enum StringsCommands { Refs(StringRefsArgs), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct StringRefsArgs { pub string: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum SymbolCommands { /// List all symbols List(QueryOptions), @@ -297,14 +302,14 @@ pub enum SymbolCommands { Rename(RenameArgs), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct SymbolGetArgs { pub name: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct CreateSymbolArgs { pub address: String, pub name: String, @@ -314,7 +319,7 @@ pub struct CreateSymbolArgs { pub project: Option, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum MemoryCommands { /// Show memory map Map(QueryOptions), @@ -326,7 +331,7 @@ pub enum MemoryCommands { Search(MemSearchArgs), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct MemReadArgs { pub address: String, pub size: usize, @@ -334,7 +339,7 @@ pub struct MemReadArgs { pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct MemWriteArgs { pub address: String, pub bytes: String, @@ -344,14 +349,14 @@ pub struct MemWriteArgs { pub project: Option, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct MemSearchArgs { pub pattern: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum XRefCommands { /// Get cross-references to address To(XRefArgs), @@ -361,14 +366,14 @@ pub enum XRefCommands { List(XRefArgs), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct XRefArgs { pub address: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum TypeCommands { /// List data types List(QueryOptions), @@ -380,14 +385,14 @@ pub enum TypeCommands { Apply(ApplyTypeArgs), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct TypeGetArgs { pub name: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct CreateTypeArgs { pub definition: String, #[arg(long)] @@ -396,7 +401,7 @@ pub struct CreateTypeArgs { pub project: Option, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct ApplyTypeArgs { pub address: String, pub type_name: String, @@ -406,7 +411,7 @@ pub struct ApplyTypeArgs { pub project: Option, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum CommentCommands { /// List all comments List(QueryOptions), @@ -418,14 +423,14 @@ pub enum CommentCommands { Delete(CommentGetArgs), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct CommentGetArgs { pub address: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct CommentSetArgs { pub address: String, pub text: String, @@ -437,7 +442,7 @@ pub struct CommentSetArgs { pub project: Option, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum FindCommands { /// Find strings String(FindStringArgs), @@ -453,35 +458,35 @@ pub enum FindCommands { Interesting(QueryOptions), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct FindStringArgs { pub pattern: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct FindBytesArgs { pub hex: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct FindFunctionArgs { pub pattern: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct FindCallsArgs { pub function: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum GraphCommands { /// Call graph Calls(QueryOptions), @@ -493,7 +498,7 @@ pub enum GraphCommands { Export(GraphExportArgs), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct GraphFunctionArgs { pub function: String, #[arg(long)] @@ -502,21 +507,21 @@ pub struct GraphFunctionArgs { pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct GraphExportArgs { pub format: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct DecompileArgs { pub target: String, #[command(flatten)] pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct DisasmArgs { pub address: String, #[arg(long)] @@ -525,7 +530,7 @@ pub struct DisasmArgs { pub options: QueryOptions, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum DiffCommands { /// Compare two programs Programs(DiffProgramsArgs), @@ -533,7 +538,7 @@ pub enum DiffCommands { Functions(QueryOptions), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct DiffProgramsArgs { pub program1: String, pub program2: String, @@ -541,7 +546,7 @@ pub struct DiffProgramsArgs { pub format: Option, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum DumpCommands { /// Dump imports Imports(QueryOptions), @@ -553,7 +558,7 @@ pub enum DumpCommands { Strings(QueryOptions), } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum PatchCommands { /// Patch bytes Bytes(PatchBytesArgs), @@ -563,7 +568,7 @@ pub enum PatchCommands { Export(PatchExportArgs), } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct PatchBytesArgs { pub address: String, pub hex: String, @@ -573,7 +578,7 @@ pub struct PatchBytesArgs { pub project: Option, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct PatchNopArgs { pub address: String, #[arg(long)] @@ -584,7 +589,7 @@ pub struct PatchNopArgs { pub project: Option, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct PatchExportArgs { #[arg(short, long)] pub output: String, @@ -594,7 +599,7 @@ pub struct PatchExportArgs { pub project: Option, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum ScriptCommands { /// Run a script file Run(ScriptRunArgs), @@ -606,7 +611,7 @@ pub enum ScriptCommands { List, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct ScriptRunArgs { pub script_path: String, #[arg(long)] @@ -618,7 +623,7 @@ pub struct ScriptRunArgs { pub args: Vec, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct ScriptInlineArgs { pub code: String, #[arg(long)] @@ -627,12 +632,12 @@ pub struct ScriptInlineArgs { pub project: Option, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct BatchArgs { pub script_file: String, } -#[derive(Subcommand)] +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum ConfigCommands { /// List all configuration List, @@ -644,32 +649,32 @@ pub enum ConfigCommands { Reset, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct SetDefaultArgs { pub kind: String, pub value: String, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct QuickArgs { pub binary: String, #[arg(long)] pub project: Option, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct SummaryArgs { #[command(flatten)] pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct StatsArgs { #[command(flatten)] pub options: QueryOptions, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct ImportArgs { pub binary: String, #[arg(long)] @@ -678,7 +683,7 @@ pub struct ImportArgs { pub project: Option, } -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct AnalyzeArgs { #[arg(long)] pub program: Option, @@ -687,7 +692,7 @@ pub struct AnalyzeArgs { } /// Common query options used across commands -#[derive(Args)] +#[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct QueryOptions { #[arg(long)] pub program: Option, @@ -719,3 +724,62 @@ pub struct QueryOptions { #[arg(long)] pub json: bool, } + +/// Daemon management commands +#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] +pub enum DaemonCommands { + /// Start the daemon + Start { + /// Project path + #[arg(long)] + project: Option, + + /// Port to listen on (default: auto-select) + #[arg(long)] + port: Option, + + /// Run in foreground (don't daemonize) + #[arg(long)] + foreground: bool, + }, + + /// Stop the daemon + Stop { + /// Project path + #[arg(long)] + project: Option, + }, + + /// Restart the daemon + Restart { + /// Project path + #[arg(long)] + project: Option, + + /// Port to listen on (default: auto-select) + #[arg(long)] + port: Option, + }, + + /// Get daemon status + Status { + /// Project path + #[arg(long)] + project: Option, + }, + + /// Ping the daemon + Ping { + /// Project path + #[arg(long)] + project: Option, + }, + + /// Clear the cache + ClearCache { + /// Project path + #[arg(long)] + project: Option, + }, +} + diff --git a/src/daemon/cache.rs b/src/daemon/cache.rs new file mode 100644 index 0000000..6752d40 --- /dev/null +++ b/src/daemon/cache.rs @@ -0,0 +1,153 @@ +//! Caching layer for common requests. +//! +//! Caches results of expensive Ghidra operations to speed up repeated queries. + +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use tokio::sync::RwLock; +use tracing::debug; + +use crate::cli::Commands; + +/// A cached entry with timestamp. +struct CacheEntry { + value: String, + inserted_at: Instant, +} + +impl CacheEntry { + fn new(value: String) -> Self { + Self { + value, + inserted_at: Instant::now(), + } + } + + fn is_expired(&self, ttl: Duration) -> bool { + self.inserted_at.elapsed() > ttl + } +} + +/// Cache for command results. +pub struct Cache { + /// Cache storage + entries: Arc>>, + /// Time-to-live for cache entries + ttl: Duration, +} + +impl Cache { + /// Create a new cache with default TTL (5 minutes). + pub fn new() -> Self { + Self::with_ttl(Duration::from_secs(300)) + } + + /// Create a new cache with custom TTL. + pub fn with_ttl(ttl: Duration) -> Self { + Self { + entries: Arc::new(RwLock::new(HashMap::new())), + ttl, + } + } + + /// Get a cached value if it exists and hasn't expired. + pub async fn get(&self, command: &Commands) -> Option { + let key = self.cache_key(command)?; + + let entries = self.entries.read().await; + if let Some(entry) = entries.get(&key) { + if !entry.is_expired(self.ttl) { + debug!("Cache hit for key: {}", key); + return Some(entry.value.clone()); + } else { + debug!("Cache entry expired for key: {}", key); + } + } + + None + } + + /// Set a cached value. + pub async fn set(&self, command: &Commands, value: String) { + if let Some(key) = self.cache_key(command) { + let mut entries = self.entries.write().await; + entries.insert(key.clone(), CacheEntry::new(value)); + debug!("Cached result for key: {}", key); + } + } + + /// Clear all cached entries. + pub async fn clear(&self) { + let mut entries = self.entries.write().await; + entries.clear(); + debug!("Cache cleared"); + } + + /// Remove expired entries. + pub async fn cleanup(&self) { + let mut entries = self.entries.write().await; + let ttl = self.ttl; + entries.retain(|_, entry| !entry.is_expired(ttl)); + debug!("Cache cleanup completed"); + } + + /// Generate a cache key for a command. + /// Only cacheable commands return Some. + fn cache_key(&self, command: &Commands) -> Option { + // For now, generate a simple cache key based on debug representation + // TODO: Implement proper cache key generation for specific command types + Some(format!("{:?}", command)) + } +} + +impl Default for Cache { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_cache_operations() { + let cache = Cache::new(); + + // Create a test command (using Version since it's simple) + let command = Commands::Version; + + // Should be empty initially + assert!(cache.get(&command).await.is_none()); + + // Set a value + cache.set(&command, "test result".to_string()).await; + + // Should return the value + assert_eq!(cache.get(&command).await, Some("test result".to_string())); + + // Clear cache + cache.clear().await; + + // Should be empty again + assert!(cache.get(&command).await.is_none()); + } + + #[tokio::test] + async fn test_cache_expiration() { + let cache = Cache::with_ttl(Duration::from_millis(100)); + + let command = Commands::Version; + + cache.set(&command, "test".to_string()).await; + assert!(cache.get(&command).await.is_some()); + + // Wait for expiration + tokio::time::sleep(Duration::from_millis(150)).await; + + // Should be expired + assert!(cache.get(&command).await.is_none()); + } +} diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs new file mode 100644 index 0000000..771f979 --- /dev/null +++ b/src/daemon/mod.rs @@ -0,0 +1,192 @@ +//! Daemon core logic. +//! +//! The daemon is the main runtime that: +//! - Loads and maintains project state in memory +//! - Queues commands to prevent Ghidra conflicts +//! - Serves RPC requests from clients +//! - Handles graceful shutdown + +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use tokio::sync::broadcast; +use tracing::{error, info, warn}; + +use crate::daemon::process::{write_daemon_info, remove_lock_file, DaemonInfo, get_data_dir}; +use crate::daemon::queue::CommandQueue; +use crate::daemon::state::DaemonState; + +pub mod cache; +pub mod process; +pub mod queue; +pub mod rpc; +pub mod state; + +/// Daemon configuration. +pub struct DaemonConfig { + /// Path to the project directory + pub project_path: PathBuf, + /// RPC port (None = auto-select) + pub port: Option, + /// Ghidra installation directory + pub ghidra_install_dir: Option, + /// Log file path + pub log_file: PathBuf, +} + +/// Run the daemon. +pub async fn run(config: DaemonConfig) -> Result<()> { + info!("Starting Ghidra daemon"); + info!("Project: {}", config.project_path.display()); + + // Get data directory + let data_dir = get_data_dir() + .context("Failed to get data directory")?; + + // Load project state + let _state = Arc::new( + DaemonState::load(&config.project_path, config.ghidra_install_dir.as_deref()) + .context("Failed to load project state")? + ); + + info!("Project state loaded successfully"); + + // Create command queue + let queue = Arc::new(CommandQueue::new(config.project_path.clone())); + + // Create shutdown channel + let (shutdown_tx, _shutdown_rx) = broadcast::channel::<()>(1); + + // Start RPC server + let port = self::rpc::run_server(queue.clone(), config.port, shutdown_tx.clone()).await + .context("Failed to start RPC server")?; + + info!("RPC server listening on port {}", port); + + // Write lock file + let daemon_info = DaemonInfo::new(&config.project_path, port, &config.log_file); + write_daemon_info(&data_dir, &config.project_path, &daemon_info) + .context("Failed to write lock file")?; + + // Start cache cleanup task + let cache_cleanup_handle = { + let queue = queue.clone(); + let shutdown_tx = shutdown_tx.clone(); + tokio::spawn(async move { + let mut shutdown_rx = shutdown_tx.subscribe(); + loop { + tokio::select! { + _ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => { + // Cleanup cache every 5 minutes + // Note: This would need access to the cache + // For now, we'll skip this as the cache is internal to the queue + } + _ = shutdown_rx.recv() => { + info!("Cache cleanup task stopping"); + break; + } + } + } + }) + }; + + // Wait for shutdown signal + let shutdown_reason = wait_for_shutdown(shutdown_tx.clone()).await; + + info!("Shutdown initiated: {:?}", shutdown_reason); + + // Clean up + shutdown_tx.send(()).ok(); // Signal all tasks to stop + + // Wait for cache cleanup to stop (with timeout) + tokio::select! { + _ = cache_cleanup_handle => { + info!("Cache cleanup task stopped"); + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => { + warn!("Cache cleanup task did not stop in time"); + } + } + + // Remove lock file + remove_lock_file(&data_dir, &config.project_path) + .context("Failed to remove lock file")?; + + info!("Daemon stopped"); + Ok(()) +} + +/// The reason for shutdown. +#[derive(Debug, Clone)] +pub enum ShutdownReason { + /// SIGINT (Ctrl+C) + Interrupt, + /// SIGTERM + Terminate, + /// RPC shutdown request + RpcRequest, +} + +/// Wait for a shutdown signal. +async fn wait_for_shutdown(shutdown_tx: broadcast::Sender<()>) -> ShutdownReason { + let mut shutdown_rx = shutdown_tx.subscribe(); + + #[cfg(unix)] + { + use tokio::signal::unix::{signal, SignalKind}; + + let mut sigint = signal(SignalKind::interrupt()) + .expect("Failed to register SIGINT handler"); + let mut sigterm = signal(SignalKind::terminate()) + .expect("Failed to register SIGTERM handler"); + + tokio::select! { + _ = sigint.recv() => { + info!("Received SIGINT"); + ShutdownReason::Interrupt + } + _ = sigterm.recv() => { + info!("Received SIGTERM"); + ShutdownReason::Terminate + } + _ = shutdown_rx.recv() => { + info!("Received shutdown request via RPC"); + ShutdownReason::RpcRequest + } + } + } + + #[cfg(windows)] + { + use tokio::signal; + + tokio::select! { + _ = signal::ctrl_c() => { + info!("Received Ctrl+C"); + ShutdownReason::Interrupt + } + _ = shutdown_rx.recv() => { + info!("Received shutdown request via RPC"); + ShutdownReason::RpcRequest + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_daemon_config() { + let config = DaemonConfig { + project_path: PathBuf::from("/test/project"), + port: Some(17700), + ghidra_install_dir: None, + log_file: PathBuf::from("/test/logs/daemon.log"), + }; + + assert_eq!(config.port, Some(17700)); + } +} diff --git a/src/daemon/process.rs b/src/daemon/process.rs new file mode 100644 index 0000000..2aa134c --- /dev/null +++ b/src/daemon/process.rs @@ -0,0 +1,180 @@ +//! Process management for the daemon. +//! +//! Handles PID files, lock files, and daemon process information. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::io::Write; + +use anyhow::{Context, Result, bail}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sysinfo::{System, Pid, ProcessRefreshKind}; + +/// Daemon information stored in the lock file. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DaemonInfo { + /// Process ID of the daemon + pub pid: u32, + /// RPC port the daemon is listening on + pub port: u16, + /// Project path being managed + pub project_path: PathBuf, + /// Log file path + pub log_file: PathBuf, + /// When the daemon was started + pub started_at: DateTime, +} + +impl DaemonInfo { + /// Create new daemon info. + pub fn new(project_path: &Path, port: u16, log_file: &Path) -> Self { + Self { + pid: std::process::id(), + port, + project_path: project_path.to_path_buf(), + log_file: log_file.to_path_buf(), + started_at: Utc::now(), + } + } +} + +/// Get the data directory for daemon files. +pub fn get_data_dir() -> Result { + let data_dir = dirs::data_local_dir() + .context("Failed to get local data directory")? + .join("ghidra-cli"); + + fs::create_dir_all(&data_dir) + .context("Failed to create data directory")?; + + Ok(data_dir) +} + +/// Get the lock file path for a project. +fn get_lock_file_path(data_dir: &Path, project_path: &Path) -> PathBuf { + let project_hash = format!("{:x}", md5::compute(project_path.to_string_lossy().as_bytes())); + data_dir.join(format!("daemon-{}.lock", project_hash)) +} + +/// Write daemon info to a lock file. +pub fn write_daemon_info(data_dir: &Path, project_path: &Path, info: &DaemonInfo) -> Result<()> { + let lock_file = get_lock_file_path(data_dir, project_path); + let json = serde_json::to_string_pretty(info) + .context("Failed to serialize daemon info")?; + + let mut file = fs::File::create(&lock_file) + .context("Failed to create lock file")?; + + file.write_all(json.as_bytes()) + .context("Failed to write lock file")?; + + Ok(()) +} + +/// Read daemon info from a lock file. +pub fn read_daemon_info(data_dir: &Path, project_path: &Path) -> Result> { + let lock_file = get_lock_file_path(data_dir, project_path); + + if !lock_file.exists() { + return Ok(None); + } + + let contents = fs::read_to_string(&lock_file) + .context("Failed to read lock file")?; + + let info: DaemonInfo = serde_json::from_str(&contents) + .context("Failed to parse lock file")?; + + Ok(Some(info)) +} + +/// Remove the lock file for a project. +pub fn remove_lock_file(data_dir: &Path, project_path: &Path) -> Result<()> { + let lock_file = get_lock_file_path(data_dir, project_path); + + if lock_file.exists() { + fs::remove_file(&lock_file) + .context("Failed to remove lock file")?; + } + + Ok(()) +} + +/// Check if a process with the given PID is running. +pub fn is_process_running(pid: u32) -> bool { + let mut sys = System::new(); + sys.refresh_processes_specifics(ProcessRefreshKind::new()); + sys.process(Pid::from_u32(pid)).is_some() +} + +/// Get daemon info if running, or clean up stale lock file. +pub fn get_running_daemon_info(data_dir: &Path, project_path: &Path) -> Result> { + if let Some(info) = read_daemon_info(data_dir, project_path)? { + if is_process_running(info.pid) { + Ok(Some(info)) + } else { + // Process is dead, clean up stale lock file + remove_lock_file(data_dir, project_path)?; + Ok(None) + } + } else { + Ok(None) + } +} + +/// Ensure no daemon is currently running for this project. +pub fn ensure_not_running(data_dir: &Path, project_path: &Path) -> Result<()> { + if let Some(info) = get_running_daemon_info(data_dir, project_path)? { + bail!( + "Daemon is already running (PID: {}, port: {})", + info.pid, + info.port + ); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn test_daemon_info_creation() { + let info = DaemonInfo::new( + Path::new("/test/project"), + 17700, + Path::new("/test/logs/daemon.log"), + ); + + assert_eq!(info.port, 17700); + assert_eq!(info.project_path, PathBuf::from("/test/project")); + } + + #[test] + fn test_lock_file_operations() -> Result<()> { + let temp_dir = tempdir()?; + let data_dir = temp_dir.path(); + let project_path = PathBuf::from("/test/project"); + + let info = DaemonInfo::new(&project_path, 17700, Path::new("/test/logs/daemon.log")); + + // Write + write_daemon_info(data_dir, &project_path, &info)?; + + // Read + let read_info = read_daemon_info(data_dir, &project_path)?; + assert!(read_info.is_some()); + let read_info = read_info.unwrap(); + assert_eq!(read_info.port, 17700); + assert_eq!(read_info.pid, info.pid); + + // Remove + remove_lock_file(data_dir, &project_path)?; + let read_info = read_daemon_info(data_dir, &project_path)?; + assert!(read_info.is_none()); + + Ok(()) + } +} diff --git a/src/daemon/queue.rs b/src/daemon/queue.rs new file mode 100644 index 0000000..9b98d71 --- /dev/null +++ b/src/daemon/queue.rs @@ -0,0 +1,165 @@ +//! Command queue for serializing Ghidra operations. +//! +//! Ensures only one Ghidra headless operation runs at a time to prevent conflicts. + +use std::collections::VecDeque; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use tokio::sync::{Mutex, Semaphore, oneshot}; +use tracing::{info, warn, error}; + +use crate::cli::Commands; +use crate::daemon::cache::Cache; + +/// A queued command waiting to be executed. +struct QueuedCommand { + command: Commands, + response_tx: oneshot::Sender>, +} + +/// Command queue for managing Ghidra operations. +pub struct CommandQueue { + /// The project path being managed + project_path: PathBuf, + /// Queue of pending commands + queue: Arc>>, + /// Semaphore to ensure only one command executes at a time + execution_lock: Arc, + /// Number of completed commands + completed_count: Arc>, + /// Cache for common requests + cache: Arc, +} + +impl CommandQueue { + /// Create a new command queue. + pub fn new(project_path: PathBuf) -> Self { + Self { + project_path, + queue: Arc::new(Mutex::new(VecDeque::new())), + execution_lock: Arc::new(Semaphore::new(1)), + completed_count: Arc::new(Mutex::new(0)), + cache: Arc::new(Cache::new()), + } + } + + /// Submit a command for execution. + pub async fn submit(&self, command: Commands) -> Result { + // Check cache first + if let Some(cached) = self.cache.get(&command).await { + info!("Cache hit for command"); + return Ok(cached); + } + + let (response_tx, response_rx) = oneshot::channel(); + + // Add to queue + { + let mut queue = self.queue.lock().await; + queue.push_back(QueuedCommand { + command: command.clone(), + response_tx, + }); + info!("Command queued (queue depth: {})", queue.len()); + } + + // Process queue + self.process_queue().await; + + // Wait for response + response_rx.await + .context("Failed to receive command response")? + } + + /// Process commands in the queue. + async fn process_queue(&self) { + let execution_lock = self.execution_lock.clone(); + let queue = self.queue.clone(); + let completed_count = self.completed_count.clone(); + let cache = self.cache.clone(); + let project_path = self.project_path.clone(); + + tokio::spawn(async move { + // Try to acquire execution lock (non-blocking) + if let Ok(_permit) = execution_lock.try_acquire() { + while let Some(queued_cmd) = { + let mut q = queue.lock().await; + q.pop_front() + } { + info!("Executing command from queue"); + + // Execute the command + let result = execute_command(&project_path, &queued_cmd.command).await; + + // Cache successful results + if let Ok(ref output) = result { + cache.set(&queued_cmd.command, output.clone()).await; + } + + // Send response + if queued_cmd.response_tx.send(result).is_err() { + warn!("Failed to send command response (receiver dropped)"); + } + + // Increment completed count + let mut count = completed_count.lock().await; + *count += 1; + } + } + }); + } + + /// Get the current queue depth. + pub fn queue_depth(&self) -> usize { + // This is a synchronous method, so we can't await the lock + // Return 0 as an estimate (actual depth available via async method) + 0 + } + + /// Get the current queue depth (async version). + pub async fn queue_depth_async(&self) -> usize { + let queue = self.queue.lock().await; + queue.len() + } + + /// Get the number of completed commands. + pub fn completed_count(&self) -> usize { + // This is a synchronous method, so we can't await the lock + // Return 0 as an estimate (actual count available via async method) + 0 + } + + /// Get the number of completed commands (async version). + pub async fn completed_count_async(&self) -> usize { + let count = self.completed_count.lock().await; + *count + } + + /// Get the project path. + pub fn project_path(&self) -> &Path { + &self.project_path + } +} + +/// Execute a command against Ghidra. +async fn execute_command(_project_path: &Path, command: &Commands) -> Result { + // TODO: Integrate with actual Ghidra execution + // For now, this is a placeholder that will be replaced with proper integration + + // For now, just return a placeholder response + Ok(format!("Command execution not yet implemented in daemon: {:?}", command)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_queue_creation() { + let queue = CommandQueue::new(PathBuf::from("/test/project")); + assert_eq!(queue.project_path(), Path::new("/test/project")); + assert_eq!(queue.queue_depth_async().await, 0); + } +} diff --git a/src/daemon/rpc.rs b/src/daemon/rpc.rs new file mode 100644 index 0000000..8d843e8 --- /dev/null +++ b/src/daemon/rpc.rs @@ -0,0 +1,275 @@ +//! RPC protocol for daemon communication using JSON over TCP. +//! +//! Defines the request/response types and RPC server/client implementations. + +use std::net::SocketAddr; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::broadcast; +use tracing::{info, error}; + +use crate::cli::Commands; +use crate::daemon::queue::CommandQueue; + +/// RPC request from client to daemon. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DaemonRequest { + /// Execute a CLI command + Execute(Commands), + /// Shutdown the daemon + Shutdown, + /// Get daemon status + Status, + /// Ping the daemon + Ping, +} + +/// RPC response from daemon to client. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DaemonResponse { + /// Command executed successfully with output + Success(String), + /// Command failed with error + Error(String), + /// Daemon status information + Status(DaemonStatus), + /// Pong response + Pong, +} + +/// Daemon status information. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DaemonStatus { + /// Number of queued commands + pub queue_depth: usize, + /// Number of completed commands + pub completed_commands: usize, + /// Daemon uptime in seconds + pub uptime_seconds: u64, + /// Current project path + pub project_path: String, +} + +/// RPC server implementation. +pub struct DaemonServer { + queue: Arc, + shutdown_tx: broadcast::Sender<()>, + started_at: std::time::Instant, +} + +impl DaemonServer { + /// Create a new RPC server. + pub fn new(queue: Arc, shutdown_tx: broadcast::Sender<()>) -> Self { + Self { + queue, + shutdown_tx, + started_at: std::time::Instant::now(), + } + } + + /// Handle a request and return a response. + async fn handle_request(&self, request: DaemonRequest) -> DaemonResponse { + match request { + DaemonRequest::Execute(command) => { + match self.queue.submit(command).await { + Ok(result) => DaemonResponse::Success(result), + Err(e) => DaemonResponse::Error(e.to_string()), + } + } + DaemonRequest::Shutdown => { + info!("Received shutdown request via RPC"); + let _ = self.shutdown_tx.send(()); + DaemonResponse::Success("Daemon shutting down".to_string()) + } + DaemonRequest::Status => { + let status = DaemonStatus { + queue_depth: 0, // TODO: Get actual queue depth + completed_commands: 0, // TODO: Get actual completed count + uptime_seconds: self.started_at.elapsed().as_secs(), + project_path: self.queue.project_path().to_string_lossy().to_string(), + }; + DaemonResponse::Status(status) + } + DaemonRequest::Ping => { + DaemonResponse::Pong + } + } + } +} + +/// Run the RPC server. +pub async fn run_server( + queue: Arc, + port: Option, + shutdown_tx: broadcast::Sender<()>, +) -> Result { + let addr = SocketAddr::from(([127, 0, 0, 1], port.unwrap_or(0))); + let listener = TcpListener::bind(addr).await + .context("Failed to bind TCP listener")?; + + let actual_port = listener.local_addr() + .context("Failed to get local address")? + .port(); + + info!("RPC server listening on port {}", actual_port); + + let server = Arc::new(DaemonServer::new(queue, shutdown_tx.clone())); + let mut shutdown_rx = shutdown_tx.subscribe(); + + tokio::spawn(async move { + loop { + tokio::select! { + result = listener.accept() => { + match result { + Ok((stream, addr)) => { + info!("Accepted connection from {}", addr); + let server = server.clone(); + tokio::spawn(async move { + if let Err(e) = handle_connection(stream, server).await { + error!("Connection error: {}", e); + } + }); + } + Err(e) => { + error!("Failed to accept connection: {}", e); + } + } + } + _ = shutdown_rx.recv() => { + info!("RPC server shutting down"); + break; + } + } + } + }); + + Ok(actual_port) +} + +/// Handle a single client connection using JSON over TCP. +async fn handle_connection( + stream: TcpStream, + server: Arc, +) -> Result<()> { + let (reader, mut writer) = stream.into_split(); + let mut reader = BufReader::new(reader); + let mut line = String::new(); + + loop { + line.clear(); + let n = reader.read_line(&mut line).await + .context("Failed to read from stream")?; + + if n == 0 { + // Connection closed + break; + } + + // Parse request + let request: DaemonRequest = serde_json::from_str(&line) + .context("Failed to parse request")?; + + // Handle request + let response = server.handle_request(request).await; + + // Serialize and send response + let response_json = serde_json::to_string(&response) + .context("Failed to serialize response")?; + + writer.write_all(response_json.as_bytes()).await + .context("Failed to write response")?; + writer.write_all(b"\n").await + .context("Failed to write newline")?; + } + + Ok(()) +} + +/// RPC client for connecting to the daemon. +pub struct DaemonClient { + stream: TcpStream, +} + +impl DaemonClient { + /// Connect to the daemon at the given port. + pub async fn connect(port: u16) -> Result { + let addr = SocketAddr::from(([127, 0, 0, 1], port)); + let stream = TcpStream::connect(addr).await + .context("Failed to connect to daemon")?; + + Ok(Self { stream }) + } + + /// Send a request to the daemon. + pub async fn request(&mut self, request: DaemonRequest) -> Result { + // Serialize and send request + let request_json = serde_json::to_string(&request) + .context("Failed to serialize request")?; + + self.stream.write_all(request_json.as_bytes()).await + .context("Failed to write request")?; + self.stream.write_all(b"\n").await + .context("Failed to write newline")?; + + // Read response + let (reader, _) = self.stream.split(); + let mut reader = BufReader::new(reader); + let mut line = String::new(); + reader.read_line(&mut line).await + .context("Failed to read response")?; + + // Parse response + let response: DaemonResponse = serde_json::from_str(&line) + .context("Failed to parse response")?; + + Ok(response) + } + + /// Execute a command on the daemon. + pub async fn execute(&mut self, command: Commands) -> Result { + match self.request(DaemonRequest::Execute(command)).await? { + DaemonResponse::Success(output) => Ok(output), + DaemonResponse::Error(e) => Err(anyhow::anyhow!(e)), + _ => Err(anyhow::anyhow!("Unexpected response")), + } + } + + /// Get daemon status. + pub async fn status(&mut self) -> Result { + match self.request(DaemonRequest::Status).await? { + DaemonResponse::Status(status) => Ok(status), + _ => Err(anyhow::anyhow!("Unexpected response")), + } + } + + /// Shutdown the daemon. + pub async fn shutdown(&mut self) -> Result<()> { + self.request(DaemonRequest::Shutdown).await?; + Ok(()) + } + + /// Ping the daemon. + pub async fn ping(&mut self) -> Result<()> { + match self.request(DaemonRequest::Ping).await? { + DaemonResponse::Pong => Ok(()), + _ => Err(anyhow::anyhow!("Unexpected response")), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_request_serialization() { + let request = DaemonRequest::Ping; + let json = serde_json::to_string(&request).unwrap(); + let deserialized: DaemonRequest = serde_json::from_str(&json).unwrap(); + matches!(deserialized, DaemonRequest::Ping); + } +} diff --git a/src/daemon/state.rs b/src/daemon/state.rs new file mode 100644 index 0000000..5ee802d --- /dev/null +++ b/src/daemon/state.rs @@ -0,0 +1,78 @@ +//! Daemon state management. +//! +//! Manages the state of loaded Ghidra projects and maintains metadata. + +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use tokio::sync::RwLock; +use tracing::info; + +use crate::config::Config; +use crate::ghidra::GhidraClient; + +/// Daemon state. +pub struct DaemonState { + /// The Ghidra client + client: Arc>, + /// Project path being managed + project_path: PathBuf, +} + +impl DaemonState { + /// Load daemon state for a project. + pub fn load(project_path: &Path, ghidra_install_dir: Option<&Path>) -> Result { + info!("Loading daemon state for project: {}", project_path.display()); + + // Load config + let mut config = Config::load() + .context("Failed to load config")?; + + // Override ghidra install dir if provided + if let Some(dir) = ghidra_install_dir { + config.ghidra_install_dir = Some(dir.to_path_buf()); + } + + // Create Ghidra client + let client = GhidraClient::new(config) + .context("Failed to create Ghidra client")?; + + // Verify the client installation is valid + client.verify_installation() + .context("Invalid Ghidra installation")?; + + info!("Daemon state loaded successfully"); + + Ok(Self { + client: Arc::new(RwLock::new(client)), + project_path: project_path.to_path_buf(), + }) + } + + /// Get a read lock on the Ghidra client. + pub async fn client(&self) -> tokio::sync::RwLockReadGuard<'_, GhidraClient> { + self.client.read().await + } + + /// Get a write lock on the Ghidra client. + pub async fn client_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, GhidraClient> { + self.client.write().await + } + + /// Get the project path. + pub fn project_path(&self) -> &Path { + &self.project_path + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_state_creation() { + // Note: This test would need a real Ghidra project to work + // In a real test environment, you'd set up a test project first + } +} diff --git a/src/main.rs b/src/main.rs index f47b850..0fba6ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod cli; mod config; +mod daemon; mod error; mod filter; mod format; @@ -7,26 +8,42 @@ mod ghidra; mod query; use clap::Parser; -use cli::{Cli, Commands, QueryArgs, QueryOptions}; +use cli::{Cli, Commands, DaemonCommands, QueryArgs, QueryOptions}; use config::Config; +use daemon::process::{get_data_dir, get_running_daemon_info, ensure_not_running}; +use daemon::rpc as daemon_rpc; +use daemon::{DaemonConfig, run as run_daemon}; use error::{GhidraError, Result}; use format::OutputFormat; use ghidra::GhidraClient; use query::{Query, DataType, FieldSelector, SortKey}; use std::path::PathBuf; +use tracing::{info, error}; -fn main() { - env_logger::init(); +#[tokio::main] +async fn main() { + // Initialize logging + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); let cli = Cli::parse(); - if let Err(e) = run(cli) { + let result = if let Commands::Daemon(_) = &cli.command { + // Daemon commands are async + run_async(cli).await + } else { + // Other commands can be sync or we check if daemon is running + run_with_daemon_check(cli).await + }; + + if let Err(e) = result { eprintln!("Error: {}", e); std::process::exit(1); } } -fn run(cli: Cli) -> Result<()> { +fn run(cli: Cli) -> anyhow::Result<()> { match cli.command { Commands::Query(args) => handle_query(args), Commands::Init => handle_init(), @@ -51,7 +68,252 @@ fn run(cli: Cli) -> Result<()> { } } -fn handle_query(args: QueryArgs) -> Result<()> { +/// Run async commands (daemon management). +async fn run_async(cli: Cli) -> anyhow::Result<()> { + match cli.command { + Commands::Daemon(cmd) => handle_daemon_command(cmd).await, + _ => unreachable!("run_async called with non-daemon command"), + } +} + +/// Run commands with daemon check - route through daemon if running. +async fn run_with_daemon_check(cli: Cli) -> anyhow::Result<()> { + let config = Config::load()?; + let data_dir = get_data_dir()?; + + // Determine project path + let project_path = match &cli.command { + Commands::Query(args) => { + let program = resolve_program(&args.program, &config)?; + PathBuf::from(resolve_project(&args.project, &config, &program)?) + } + Commands::Import(args) => { + let program = resolve_program(&args.program, &config)?; + PathBuf::from(resolve_project(&args.project, &config, &program)?) + } + Commands::Analyze(args) => { + let program = resolve_program(&args.program, &config)?; + PathBuf::from(resolve_project(&args.project, &config, &program)?) + } + _ => { + // For commands that don't specify project, use default or run directly + if let Some(ref proj) = config.default_project { + PathBuf::from(proj) + } else { + // No project specified, run command directly + return run(cli); + } + } + }; + + // Check if daemon is running for this project + if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? { + info!("Daemon is running, routing command through daemon (port: {})", daemon_info.port); + + // Connect to daemon and execute command + let mut client = daemon_rpc::DaemonClient::connect(daemon_info.port).await?; + let output = client.execute(cli.command).await?; + println!("{}", output); + Ok(()) + } else { + // No daemon running, execute directly + run(cli) + } +} + +/// Handle daemon management commands. +async fn handle_daemon_command(cmd: DaemonCommands) -> anyhow::Result<()> { + match cmd { + DaemonCommands::Start { project, port, foreground } => { + handle_daemon_start(project, port, foreground).await + } + DaemonCommands::Stop { project } => { + handle_daemon_stop(project).await + } + DaemonCommands::Restart { project, port } => { + handle_daemon_restart(project, port).await + } + DaemonCommands::Status { project } => { + handle_daemon_status(project).await + } + DaemonCommands::Ping { project } => { + handle_daemon_ping(project).await + } + DaemonCommands::ClearCache { project } => { + handle_daemon_clear_cache(project).await + } + } +} + +/// Start the daemon. +async fn handle_daemon_start(project: Option, port: Option, foreground: bool) -> anyhow::Result<()> { + let config = Config::load()?; + let data_dir = get_data_dir()?; + + // Resolve project path + let project_path = if let Some(proj) = project { + PathBuf::from(proj) + } else if let Some(ref default_proj) = config.default_project { + PathBuf::from(default_proj) + } else { + anyhow::bail!("No project specified and no default project configured"); + }; + + // Check if daemon is already running + ensure_not_running(&data_dir, &project_path)?; + + // Create log file path + let log_file = data_dir.join("daemon.log"); + + let daemon_config = DaemonConfig { + project_path: project_path.clone(), + port, + ghidra_install_dir: config.ghidra_install_dir.map(PathBuf::from), + log_file, + }; + + if foreground { + // Run in foreground + println!("Starting daemon in foreground mode..."); + run_daemon(daemon_config).await?; + } else { + // TODO: Daemonize properly (fork, detach, etc.) + // For now, just run in foreground + println!("Starting daemon for project: {}", project_path.display()); + println!("Note: Background mode not yet implemented, running in foreground"); + run_daemon(daemon_config).await?; + } + + Ok(()) +} + +/// Stop the daemon. +async fn handle_daemon_stop(project: Option) -> anyhow::Result<()> { + let config = Config::load()?; + let data_dir = get_data_dir()?; + + let project_path = if let Some(proj) = project { + PathBuf::from(proj) + } else if let Some(ref default_proj) = config.default_project { + PathBuf::from(default_proj) + } else { + anyhow::bail!("No project specified and no default project configured"); + }; + + if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? { + println!("Stopping daemon (PID: {}, port: {})...", daemon_info.pid, daemon_info.port); + + // Connect and send shutdown + let mut client = daemon_rpc::DaemonClient::connect(daemon_info.port).await?; + client.shutdown().await?; + + println!("Daemon stopped successfully"); + } else { + println!("No daemon running for project: {}", project_path.display()); + } + + Ok(()) +} + +/// Restart the daemon. +async fn handle_daemon_restart(project: Option, port: Option) -> anyhow::Result<()> { + // Stop first + handle_daemon_stop(project.clone()).await?; + + // Wait a moment + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + // Start again + handle_daemon_start(project, port, false).await +} + +/// Get daemon status. +async fn handle_daemon_status(project: Option) -> anyhow::Result<()> { + let config = Config::load()?; + let data_dir = get_data_dir()?; + + let project_path = if let Some(proj) = project { + PathBuf::from(proj) + } else if let Some(ref default_proj) = config.default_project { + PathBuf::from(default_proj) + } else { + anyhow::bail!("No project specified and no default project configured"); + }; + + if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? { + println!("Daemon is running:"); + println!(" PID: {}", daemon_info.pid); + println!(" Port: {}", daemon_info.port); + println!(" Project: {}", daemon_info.project_path.display()); + println!(" Started: {}", daemon_info.started_at); + println!(" Log file: {}", daemon_info.log_file.display()); + + // Try to get detailed status from daemon + if let Ok(mut client) = daemon_rpc::DaemonClient::connect(daemon_info.port).await { + if let Ok(status) = client.status().await { + println!("\nDaemon status:"); + println!(" Queue depth: {}", status.queue_depth); + println!(" Completed commands: {}", status.completed_commands); + println!(" Uptime: {} seconds", status.uptime_seconds); + } + } + } else { + println!("No daemon running for project: {}", project_path.display()); + } + + Ok(()) +} + +/// Ping the daemon. +async fn handle_daemon_ping(project: Option) -> anyhow::Result<()> { + let config = Config::load()?; + let data_dir = get_data_dir()?; + + let project_path = if let Some(proj) = project { + PathBuf::from(proj) + } else if let Some(ref default_proj) = config.default_project { + PathBuf::from(default_proj) + } else { + anyhow::bail!("No project specified and no default project configured"); + }; + + if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? { + let mut client = daemon_rpc::DaemonClient::connect(daemon_info.port).await?; + client.ping().await?; + println!("Daemon is responsive (port: {})", daemon_info.port); + } else { + println!("No daemon running for project: {}", project_path.display()); + } + + Ok(()) +} + +/// Clear daemon cache. +async fn handle_daemon_clear_cache(project: Option) -> anyhow::Result<()> { + let config = Config::load()?; + let data_dir = get_data_dir()?; + + let project_path = if let Some(proj) = project { + PathBuf::from(proj) + } else if let Some(ref default_proj) = config.default_project { + PathBuf::from(default_proj) + } else { + anyhow::bail!("No project specified and no default project configured"); + }; + + if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? { + // TODO: Implement cache clear via RPC + println!("Cache clear not yet implemented via RPC"); + // For now, just notify + println!("Note: Cache will naturally expire after TTL"); + } else { + println!("No daemon running for project: {}", project_path.display()); + } + + Ok(()) +} + +fn handle_query(args: QueryArgs) -> anyhow::Result<()> { let config = Config::load()?; let client = GhidraClient::new(config.clone())?; @@ -116,7 +378,7 @@ fn handle_query(args: QueryArgs) -> Result<()> { Ok(()) } -fn handle_function_command(cmd: cli::FunctionCommands) -> Result<()> { +fn handle_function_command(cmd: cli::FunctionCommands) -> anyhow::Result<()> { use cli::FunctionCommands; match cmd { @@ -160,7 +422,7 @@ fn handle_function_command(cmd: cli::FunctionCommands) -> Result<()> { } } -fn handle_decompile(args: cli::DecompileArgs) -> Result<()> { +fn handle_decompile(args: cli::DecompileArgs) -> anyhow::Result<()> { let query_args = QueryArgs { data_type: "functions".to_string(), program: args.options.program, @@ -177,7 +439,7 @@ fn handle_decompile(args: cli::DecompileArgs) -> Result<()> { handle_decompile_impl(args.target, query_args) } -fn handle_decompile_impl(target: String, args: QueryArgs) -> Result<()> { +fn handle_decompile_impl(target: String, args: QueryArgs) -> anyhow::Result<()> { let config = Config::load()?; let client = GhidraClient::new(config.clone())?; @@ -200,7 +462,7 @@ fn handle_decompile_impl(target: String, args: QueryArgs) -> Result<()> { Ok(()) } -fn handle_strings_command(cmd: cli::StringsCommands) -> Result<()> { +fn handle_strings_command(cmd: cli::StringsCommands) -> anyhow::Result<()> { use cli::StringsCommands; match cmd { @@ -227,7 +489,7 @@ fn handle_strings_command(cmd: cli::StringsCommands) -> Result<()> { } } -fn handle_memory_command(cmd: cli::MemoryCommands) -> Result<()> { +fn handle_memory_command(cmd: cli::MemoryCommands) -> anyhow::Result<()> { use cli::MemoryCommands; match cmd { @@ -254,7 +516,7 @@ fn handle_memory_command(cmd: cli::MemoryCommands) -> Result<()> { } } -fn handle_dump_command(cmd: cli::DumpCommands) -> Result<()> { +fn handle_dump_command(cmd: cli::DumpCommands) -> anyhow::Result<()> { use cli::DumpCommands; match cmd { @@ -325,7 +587,7 @@ fn handle_dump_command(cmd: cli::DumpCommands) -> Result<()> { } } -fn handle_init() -> Result<()> { +fn handle_init() -> anyhow::Result<()> { println!("Ghidra CLI Initialization"); println!("========================\n"); @@ -364,7 +626,7 @@ fn handle_init() -> Result<()> { Ok(()) } -fn handle_doctor() -> Result<()> { +fn handle_doctor() -> anyhow::Result<()> { println!("Ghidra CLI Doctor"); println!("=================\n"); @@ -429,13 +691,13 @@ fn handle_doctor() -> Result<()> { Ok(()) } -fn handle_version() -> Result<()> { +fn handle_version() -> anyhow::Result<()> { println!("ghidra-cli {}", env!("CARGO_PKG_VERSION")); println!("Rust CLI for Ghidra reverse engineering"); Ok(()) } -fn handle_import(args: cli::ImportArgs) -> Result<()> { +fn handle_import(args: cli::ImportArgs) -> anyhow::Result<()> { let config = Config::load()?; let client = GhidraClient::new(config.clone())?; @@ -443,7 +705,7 @@ fn handle_import(args: cli::ImportArgs) -> Result<()> { let binary_path = PathBuf::from(&args.binary); if !binary_path.exists() { - return Err(GhidraError::Other(format!("Binary not found: {}", args.binary))); + anyhow::bail!(format!("Binary not found: {}", args.binary)); } println!("Importing {} into project {}...", args.binary, project); @@ -455,7 +717,7 @@ fn handle_import(args: cli::ImportArgs) -> Result<()> { Ok(()) } -fn handle_analyze(args: cli::AnalyzeArgs) -> Result<()> { +fn handle_analyze(args: cli::AnalyzeArgs) -> anyhow::Result<()> { let config = Config::load()?; let client = GhidraClient::new(config.clone())?; @@ -471,7 +733,7 @@ fn handle_analyze(args: cli::AnalyzeArgs) -> Result<()> { Ok(()) } -fn handle_summary(opts: QueryOptions) -> Result<()> { +fn handle_summary(opts: QueryOptions) -> anyhow::Result<()> { let config = Config::load()?; let client = GhidraClient::new(config.clone())?; @@ -504,7 +766,7 @@ fn handle_summary(opts: QueryOptions) -> Result<()> { Ok(()) } -fn handle_quick(args: cli::QuickArgs) -> Result<()> { +fn handle_quick(args: cli::QuickArgs) -> anyhow::Result<()> { let config = Config::load()?; let client = GhidraClient::new(config.clone())?; @@ -540,7 +802,7 @@ fn handle_quick(args: cli::QuickArgs) -> Result<()> { Ok(()) } -fn handle_config_command(cmd: cli::ConfigCommands) -> Result<()> { +fn handle_config_command(cmd: cli::ConfigCommands) -> anyhow::Result<()> { use cli::ConfigCommands; match cmd { @@ -568,7 +830,7 @@ fn handle_config_command(cmd: cli::ConfigCommands) -> Result<()> { config.timeout = Some(timeout); } _ => { - return Err(GhidraError::ConfigError(format!("Unknown config key: {}", key))); + anyhow::bail!("Unknown config key: {}", key); } } config.save()?; @@ -584,7 +846,7 @@ fn handle_config_command(cmd: cli::ConfigCommands) -> Result<()> { Ok(()) } -fn handle_set_default(args: cli::SetDefaultArgs) -> Result<()> { +fn handle_set_default(args: cli::SetDefaultArgs) -> anyhow::Result<()> { let mut config = Config::load()?; match args.kind.as_str() { @@ -599,14 +861,14 @@ fn handle_set_default(args: cli::SetDefaultArgs) -> Result<()> { println!("Default project set to: {}", args.value); } _ => { - return Err(GhidraError::Other(format!("Unknown default kind: {}", args.kind))); + anyhow::bail!(format!("Unknown default kind: {}", args.kind)); } } Ok(()) } -fn handle_project_command(cmd: cli::ProjectCommands) -> Result<()> { +fn handle_project_command(cmd: cli::ProjectCommands) -> anyhow::Result<()> { use cli::ProjectCommands; let config = Config::load()?;