use proc_macro::TokenStream;
use quote::quote;
use syn::{FnArg, Item, ItemFn, Pat, PatIdent, parse_macro_input};
#[proc_macro_attribute]
pub fn wire(_args: TokenStream, input: TokenStream) -> TokenStream {
let item = parse_macro_input!(input as Item);
quote! {
#[derive(
cindy::__reexports::serde::Serialize,
cindy::__reexports::serde::Deserialize
)]
#[serde(crate = "cindy::__reexports::serde")]
#item
}
.into()
}
#[proc_macro_attribute]
pub fn main(_args: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemFn);
let (attrs, vis, sig, block) = (&input.attrs, &input.vis, &input.sig, &input.block);
let output = &sig.output;
let inputs = &sig.inputs;
let invoke_user_main = match inputs.len() {
0 => quote! {
cindy::__reexports::tokio::spawn(__user_main())
},
1 => {
let host_type = match &inputs[0] {
FnArg::Typed(pt) => &pt.ty,
FnArg::Receiver(_) => {
panic!("`#[cindy::main]` cannot take `self`");
}
};
quote! {
{
let __host_json = ::std::env::var("CINDY_HOST_CONTEXT").expect(
"CINDY_HOST_CONTEXT not set!\n\
The orchestrator process is meant to be launched by `cindy` command line tool. \
If you're trying to run the binary directly, set CINDY_HOST_CONTEXT to a \
JSON-serialised `cindy::Host<V>` first."
);
let __host: #host_type =
cindy::__reexports::serde_json::from_str(&__host_json)
.expect("CINDY_HOST_CONTEXT was not valid JSON for the declared `cindy::Host<V>` type");
cindy::__reexports::tokio::spawn(__user_main(__host))
}
}
}
_ => panic!(
"`#[cindy::main]` accepts at most one parameter (the host context, `cindy::Host<V>`)"
),
};
quote! {
#(#attrs)*
#[cindy::__reexports::tokio::main(crate = "cindy::__reexports::tokio")]
#vis async fn main() {
let (rpc_in, rpc_out) = cindy::common::quarantine_stdio();
#[cfg(feature = "orchestrator")]
if ::std::env::var_os("CINDY_DUMP_INVENTORY").is_some() {
let entries: ::std::vec::Vec<&cindy::inventory_types::RegisteredInventory> =
cindy::__reexports::inventory::iter::<cindy::inventory_types::RegisteredInventory>
.into_iter()
.collect();
let value: cindy::__reexports::serde_json::Value = match entries.as_slice() {
[one] => (one.dump)().await,
[] => {
::std::eprintln!(
"no `#[cindy::inventory]` registered in this binary — \
define exactly one inventory function"
);
::std::process::exit(2);
}
_ => {
::std::eprintln!(
"multiple `#[cindy::inventory]` functions registered \
({} found); exactly one is supported",
entries.len()
);
::std::process::exit(2);
}
};
let bytes = cindy::__reexports::serde_json::to_vec(&value)
.expect("Failed to serialise inventory to JSON");
{
use cindy::__reexports::tokio::io::AsyncWriteExt as _;
let mut out = rpc_out;
out.write_all(&bytes).await.expect("Failed to write inventory");
out.flush().await.expect("Failed to flush inventory");
}
::std::process::exit(0);
}
#[cfg(feature = "orchestrator")]
if ::std::env::var_os("CINDY_SEAL_SECRETS").is_some() {
use cindy::__reexports::tokio::io::AsyncWriteExt as _;
let mut out = rpc_out;
let mut failed = false;
for pending in cindy::__reexports::inventory::iter::<cindy::secret::PendingSecret>() {
let plaintext = (pending.serialize)();
let dek = match cindy::secret::keychain::get_dek(pending.vault) {
Ok(d) => d,
Err(e) => {
::std::eprintln!(
"cindy secret seal: couldn't load DEK for vault `{}` \
(referenced from {}:{}:{}): {e:#}",
pending.vault, pending.file, pending.line, pending.column,
);
failed = true;
continue;
}
};
let ciphertext = match cindy::secret::crypto::seal(&dek, &plaintext) {
Ok(c) => c,
Err(e) => {
::std::eprintln!(
"cindy secret seal: encryption failed for {}:{}:{} ({e:#})",
pending.file, pending.line, pending.column,
);
failed = true;
continue;
}
};
use cindy::__reexports::base64::Engine as _;
let b64 = cindy::__reexports::base64::engine::general_purpose::STANDARD
.encode(&ciphertext);
let line = cindy::__reexports::serde_json::json!({
"file": pending.file,
"line": pending.line,
"column": pending.column,
"vault": pending.vault,
"ciphertext": b64,
});
let mut bytes = cindy::__reexports::serde_json::to_vec(&line)
.expect("Failed to serialise seal record");
bytes.push(b'\n');
out.write_all(&bytes).await.expect("Failed to write seal record");
}
out.flush().await.expect("Failed to flush seal records");
::std::process::exit(if failed { 2 } else { 0 });
}
#[cfg(all(feature = "remote", not(feature = "orchestrator")))]
{
cindy::remote::rpc(rpc_in, rpc_out).await;
::std::process::exit(0);
}
#[cfg(feature = "orchestrator")]
{
async fn __user_main(#inputs) #output #block
let (tx, rx) = cindy::__reexports::tokio::sync::mpsc::unbounded_channel();
cindy::orchestrator::ORCHESTRATOR_TX
.set(tx)
.expect("ORCHESTRATOR_TX already set");
cindy::__reexports::tokio::spawn(cindy::orchestrator::rpc(rx, rpc_in, rpc_out));
match #invoke_user_main.await {
Ok(Ok(_)) => {
::std::process::exit(0);
}
Ok(Err(e)) => {
::std::eprintln!("\x1b[31m{:?}\x1b[0m", e);
::std::process::exit(1);
}
Err(_) => {
::std::process::exit(1);
}
};
}
}
}
.into()
}
#[proc_macro_attribute]
pub fn inventory(_args: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemFn);
let (attrs, vis, sig, block) = (&input.attrs, &input.vis, &input.sig, &input.block);
let function_ident = &sig.ident;
let asyncness = &sig.asyncness;
let is_async = asyncness.is_some();
let output = &sig.output;
let inputs = &sig.inputs;
if !inputs.is_empty() {
panic!("`#[cindy::inventory]` functions must take no arguments");
}
let invoke = if is_async {
quote! { #function_ident().await }
} else {
quote! {
match cindy::__reexports::tokio::task::spawn_blocking(|| #function_ident())
.await
{
Ok(v) => v,
Err(je) => ::std::panic::resume_unwind(je.into_panic()),
}
}
};
quote! {
#(#attrs)*
#vis #asyncness fn #function_ident () #output #block
cindy::__reexports::inventory::submit! {
cindy::inventory_types::RegisteredInventory {
dump: || ::std::boxed::Box::pin(async move {
let result = #invoke;
<_ as cindy::inventory_types::IntoInventoryDump>::into_inventory_dump(result)
}),
}
}
}
.into()
}
#[proc_macro_attribute]
pub fn remote(_args: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemFn);
let (attrs, vis, sig, block) = (&input.attrs, &input.vis, &input.sig, &input.block);
let function_ident = &sig.ident;
let asyncness = &sig.asyncness;
let is_async = asyncness.is_some();
let inputs = &sig.inputs;
let return_type = match &sig.output {
syn::ReturnType::Default => quote! { () },
syn::ReturnType::Type(_, ty) => quote! { #ty },
};
let mut arg_idents = vec![];
let mut arg_types = vec![];
for input_arg in &sig.inputs {
match input_arg {
FnArg::Receiver(..) => panic!("Argument `self` not allowed"),
FnArg::Typed(pat_type) => match &*pat_type.pat {
Pat::Ident(PatIdent { ident, .. }) => {
arg_idents.push(ident);
arg_types.push(&pat_type.ty)
}
other => panic!("Only standard named arguments are supported. Found: {other:?}"),
},
}
}
let type_signature = arg_types
.iter()
.map(|ty| quote! { #ty }.to_string().replace(' ', ""))
.collect::<Vec<String>>()
.join(",");
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
let absolute_span_path = std::path::PathBuf::from(proc_macro::Span::call_site().file());
let relative_path = absolute_span_path
.strip_prefix(&manifest_dir)
.unwrap_or(&absolute_span_path)
.to_string_lossy()
.to_string();
let crate_name = std::env::var("CARGO_PKG_NAME").unwrap_or_default();
let remote_fn_id = format!(
"::{}::{}::{}({})",
crate_name, relative_path, function_ident, type_signature
);
let invoke_user_fn = if is_async {
quote! { #function_ident::inner( #(#arg_idents),* ).await }
} else {
quote! {
match cindy::__reexports::tokio::task::spawn_blocking(move || {
#function_ident::inner( #(#arg_idents),* )
})
.await
{
Ok(v) => v,
Err(je) => ::std::panic::resume_unwind(je.into_panic()),
}
}
};
quote! {
#[allow(non_camel_case_types)]
#[doc(hidden)]
#vis enum #function_ident {}
#[doc(hidden)]
impl #function_ident {
#(#attrs)*
pub #asyncness fn inner (#inputs) -> #return_type #block
}
#[cfg(feature = "orchestrator")]
#(#attrs)*
#vis fn #function_ident (#inputs) -> cindy::orchestrator::Future<#return_type> {
let uuid = cindy::__reexports::uuid::Uuid::new_v4();
let payload = cindy::common::RemoteFnPayload {
uuid,
fn_id: #remote_fn_id.to_string(),
data: cindy::__reexports::postcard::to_allocvec(&( #(#arg_idents),* ))
.expect("Failed to serialize args"),
};
let (tx, rx) = cindy::__reexports::tokio::sync::oneshot::channel();
cindy::orchestrator::ORCHESTRATOR_TX
.get()
.expect("ORCHESTRATOR_TX not set")
.send(cindy::orchestrator::OutboundRegistration { payload, tx })
.expect("Orchestrator channel closed");
cindy::orchestrator::Future::new(rx)
}
#[cfg(feature = "remote")]
cindy::__reexports::inventory::submit! {
cindy::remote::RemoteFn {
id: #remote_fn_id,
function: |args_bytes| {
let ( #(#arg_idents),* ): ( #(#arg_types),* ) =
cindy::__reexports::postcard::from_bytes(&args_bytes)
.expect("Failed to deserialize args");
::std::boxed::Box::pin(async move {
let result = #invoke_user_fn;
cindy::__reexports::postcard::to_allocvec(&result)
.expect("Failed to serialize return value")
})
},
}
}
}
.into()
}