use std::collections::BTreeMap;
use std::path::Path;
use std::sync::Arc;
use aion_awl::{CompiledWorkerAction, CompiledWorkerBody, CompiledWorkerDocument, RunCapture};
use aion_package::ActivityDescriptor;
use aion_package::contract::CommandBodyCapture;
use aion_worker::shell::{
DeclaredCommandAction, ShellAction, resolve_working_directory, shape_command_result,
};
use aion_worker::{ActivityContext, ActivityFailure, Worker, WorkerConfig};
use anyhow::{Context, Result};
use serde_json::Value;
use super::args::{self, AwlWorkerArgs, ConnectionSettings};
use crate::settings::Environment;
pub(super) struct ServedAction {
name: String,
body: ServedBody,
}
pub(super) enum ServedBody {
Run {
command: ShellAction,
capture: RunCapture,
},
Command {
command: Box<DeclaredCommandAction>,
capture: CommandBodyCapture,
},
}
pub(crate) async fn run(args: &AwlWorkerArgs, endpoint: &str) -> Result<()> {
let compiled = compile(&args.document)?;
if args.check {
return report(&mut std::io::stdout(), &compiled);
}
let environment = Environment::from_process();
let settings = args::resolve(args, &compiled.task_queue, &environment)?;
build_worker(
&compiled,
&settings,
endpoint,
crate::awl::document_root(&args.document),
)?
.run()
.await?;
Ok(())
}
pub(super) fn compile(document: &Path) -> Result<CompiledWorkerDocument> {
let source = std::fs::read_to_string(document)
.with_context(|| format!("failed to read AWL document {}", document.display()))?;
aion_awl::compile_worker_document(&source, crate::awl::document_root(document)).map_err(
|error| {
anyhow::anyhow!(
"{} cannot be served:\n{}",
document.display(),
crate::awl::compile_diagnostics(document, &error).join("\n")
)
},
)
}
fn descriptors(compiled: &CompiledWorkerDocument) -> BTreeMap<String, ActivityDescriptor> {
compiled
.contract
.actions
.iter()
.map(|action| {
(
action.name.clone(),
ActivityDescriptor {
name: action.name.clone(),
input_schema: action.input_schema.clone(),
output_schema: action.output_schema.clone(),
},
)
})
.collect()
}
pub(super) fn report(
writer: &mut impl std::io::Write,
compiled: &CompiledWorkerDocument,
) -> Result<()> {
let descriptors = descriptors(compiled);
let mut actions = Vec::with_capacity(compiled.actions.len());
for action in &compiled.actions {
let descriptor = descriptors.get(&action.name).with_context(|| {
format!(
"no derived descriptor for action `{}`; the action surface is incomplete",
action.name
)
})?;
let (form, capture, command) = match &action.body {
CompiledWorkerBody::Run {
command, capture, ..
} => ("run", capture_word(*capture), serde_json::json!(command)),
CompiledWorkerBody::Command { command, capture } => (
"runs command",
command_capture_word(*capture),
serde_json::to_value(command.as_ref())
.context("failed to encode a declared command body")?,
),
};
actions.push(serde_json::json!({
"name": action.name,
"form": form,
"capture": capture,
"command": command,
"input_schema": descriptor.input_schema,
"output_schema": descriptor.output_schema,
}));
}
let report = serde_json::json!({
"task_queue": compiled.task_queue,
"actions": actions,
});
serde_json::to_writer_pretty(&mut *writer, &aion_package::CanonicalJson::new(report))
.context("failed to encode the advertised action surface")?;
writeln!(writer).context("failed to write the advertised action surface")?;
Ok(())
}
const fn capture_word(capture: RunCapture) -> &'static str {
match capture {
RunCapture::Outcome => "outcome",
RunCapture::Text => "text",
RunCapture::Json => "json",
}
}
const fn command_capture_word(capture: CommandBodyCapture) -> &'static str {
match capture {
CommandBodyCapture::Text => "text",
CommandBodyCapture::Json => "json",
}
}
pub(super) fn build_worker(
compiled: &CompiledWorkerDocument,
settings: &ConnectionSettings,
endpoint: &str,
workspace_root: &Path,
) -> Result<Worker> {
let mut descriptors = descriptors(compiled);
let config = WorkerConfig::builder()
.endpoint(crate::normalize_endpoint(endpoint))
.task_queue(&compiled.task_queue)
.identity(&settings.identity)
.max_concurrency(settings.concurrency)
.reconnect_initial_backoff(settings.initial_backoff)
.reconnect_max_backoff(settings.max_backoff)
.reconnect_max_attempts(settings.max_attempts)
.build()?;
let mut builder = Worker::builder(config);
for action in &compiled.actions {
let name = action.name.clone();
let descriptor = descriptors.remove(&name).with_context(|| {
format!("no derived descriptor for action `{name}`; the action surface is incomplete")
})?;
let served = Arc::new(served_action(action, workspace_root)?);
builder = builder.register_activity_with_descriptor(
name,
descriptor,
move |input: Value, context| {
let served = Arc::clone(&served);
Box::pin(async move { execute(&served, &input, context).await })
},
)?;
}
builder.build().map_err(Into::into)
}
pub(super) fn served_action(
action: &CompiledWorkerAction,
workspace_root: &Path,
) -> Result<ServedAction> {
let body = match &action.body {
CompiledWorkerBody::Run {
command, capture, ..
} => ServedBody::Run {
command: ShellAction::new(command).with_context(|| {
format!(
"action `{}` declares a command the executor cannot parse",
action.name
)
})?,
capture: *capture,
},
CompiledWorkerBody::Command { command, capture } => {
let prepared = DeclaredCommandAction::new(command.as_ref().clone());
let prepared = match prepared.declared_working_directory() {
Some(declared) => {
let resolved = resolve_working_directory(declared, workspace_root)
.with_context(|| {
format!(
"action `{}` declares a working directory this worker cannot resolve",
action.name
)
})?;
prepared.with_working_directory(resolved)
}
None => prepared,
};
ServedBody::Command {
command: Box::new(prepared),
capture: *capture,
}
}
};
Ok(ServedAction {
name: action.name.clone(),
body,
})
}
pub(super) async fn execute(
served: &ServedAction,
input: &Value,
context: &ActivityContext,
) -> Result<Value, ActivityFailure> {
let arguments = decode_arguments(&served.name, input)?;
match &served.body {
ServedBody::Run { command, capture } => {
let outcome = command.run(&arguments, context).await?;
match capture {
RunCapture::Outcome => serde_json::to_value(&outcome).map_err(|error| {
ActivityFailure::terminal(format!(
"action `{}` could not encode its command outcome: {error}",
served.name
))
}),
RunCapture::Text => Ok(Value::String(outcome.stdout)),
RunCapture::Json => serde_json::from_str(&outcome.stdout).map_err(|error| {
ActivityFailure::terminal(format!(
"action `{}` declares a `run json` body and its command printed output \
that is not valid JSON: {error}",
served.name
))
}),
}
}
ServedBody::Command { command, capture } => {
let outcome = command.run(&arguments, context).await?;
shape_command_result(&served.name, *capture, outcome)
}
}
}
pub(super) fn decode_arguments(
action: &str,
input: &Value,
) -> Result<BTreeMap<String, Value>, ActivityFailure> {
match input {
Value::Object(members) => Ok(members.clone().into_iter().collect()),
other => Err(ActivityFailure::terminal(format!(
"action `{action}` was dispatched input that is not an object binding its \
parameters by name; got {}",
json_kind(other)
))),
}
}
const fn json_kind(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "an array",
Value::Object(_) => "an object",
}
}