use crate::core::types::{Resource, TaskMode};
pub(crate) fn extract_absolute_binary(cmd: &str) -> Option<&str> {
for token in cmd.split_whitespace() {
if token.contains('=') && !token.starts_with('/') {
continue;
}
if matches!(token, "nohup" | "sudo" | "bash" | "sh" | "env" | "exec") {
continue;
}
if token.starts_with('/') {
return Some(token);
}
break;
}
None
}
pub fn check_script(resource: &Resource) -> String {
if resource.task_mode.as_ref() == Some(&TaskMode::Service) {
let rid = resource.name.as_deref().unwrap_or("task");
let pidfile = format!("/tmp/forjar-svc-{rid}.pid");
let ldd_check = extract_absolute_binary(resource.command.as_deref().unwrap_or(""))
.map(|bin| {
format!(
"if command -v ldd >/dev/null 2>&1 && [ -f '{bin}' ]; then \
if ldd '{bin}' 2>&1 | grep -q 'not found'; then \
echo 'task=ldd-fail'; exit 1; fi; fi; "
)
})
.unwrap_or_default();
return format!(
"{ldd_check}\
if [ -f '{pidfile}' ] && kill -0 \"$(cat '{pidfile}')\" 2>/dev/null; then \
echo 'task=completed'; else echo 'task=pending'; fi"
);
}
if let Some(ref check) = resource.completion_check {
return format!("if {check}; then echo 'task=completed'; else echo 'task=pending'; fi");
}
if !resource.output_artifacts.is_empty() {
let checks: Vec<String> = resource
.output_artifacts
.iter()
.map(|a| format!("[ -e '{a}' ]"))
.collect();
return format!(
"if {} ; then echo 'task=completed'; else echo 'task=pending'; fi",
checks.join(" && ")
);
}
"echo 'task=pending'".to_string()
}
fn pipeline_script(resource: &Resource) -> String {
let mut script = String::from("set -euo pipefail\n");
if let Some(ref dir) = resource.working_dir {
script.push_str(&format!("cd '{dir}'\n"));
}
script.push_str("FORJAR_PIPELINE_OK=0\n");
for (i, stage) in resource.stages.iter().enumerate() {
let cmd = stage.command.as_deref().unwrap_or("true");
let name = if stage.name.is_empty() {
format!("stage-{i}")
} else {
stage.name.clone()
};
script.push_str(&format!("echo '=== Stage: {name} ==='\n"));
if stage.gate {
script.push_str(&format!(
"if ! bash -c '{cmd}'; then\n echo 'GATE FAILED: {name}'\n exit 1\nfi\n"
));
} else {
script.push_str(&format!("{cmd}\n"));
}
}
script
}
pub fn apply_script(resource: &Resource) -> String {
if !resource.stages.is_empty() {
return pipeline_script(resource);
}
match resource.task_mode.as_ref().unwrap_or(&TaskMode::Batch) {
TaskMode::Service => return service_script(resource),
TaskMode::Dispatch => return dispatch_script(resource),
TaskMode::Pipeline | TaskMode::Batch => {} }
batch_script(resource)
}
fn batch_script(resource: &Resource) -> String {
let command = resource.command.as_deref().unwrap_or("true");
let mut script = String::from("set -euo pipefail\n");
if let Some(scatter) = scatter_script(resource) {
script.push_str(&scatter);
}
if let Some(ref dir) = resource.working_dir {
script.push_str(&format!("cd '{dir}'\n"));
}
if let Some(timeout_secs) = resource.timeout {
script.push_str(&format!(
"timeout {timeout_secs} bash <<'FORJAR_TIMEOUT'\n{command}\nFORJAR_TIMEOUT\n"
));
} else {
script.push_str(command);
script.push('\n');
}
if let Some(gather) = gather_script(resource) {
script.push_str(&gather);
}
script
}
fn service_script(resource: &Resource) -> String {
let command = resource.command.as_deref().unwrap_or("true");
let rid = resource.name.as_deref().unwrap_or("task");
let pidfile = format!("/tmp/forjar-svc-{rid}.pid");
let mut script = String::from("set -euo pipefail\n");
if let Some(ref dir) = resource.working_dir {
script.push_str(&format!("cd '{dir}'\n"));
}
script.push_str(&format!(
"if [ -f '{pidfile}' ] && kill -0 \"$(cat '{pidfile}')\" 2>/dev/null; then\n\
\x20 echo 'service={rid} already running (pid='\"$(cat '{pidfile}')\"')'\n\
\x20 exit 0\nfi\n"
));
script.push_str(&format!(
"nohup bash -c '{command}' > /tmp/forjar-svc-{rid}.log 2>&1 &\n\
FORJAR_SVC_PID=$!\n\
echo $FORJAR_SVC_PID > '{pidfile}'\n\
echo 'service={rid} started (pid='$FORJAR_SVC_PID')'\n"
));
if let Some(ref hc) = resource.health_check {
let timeout = hc
.timeout
.as_deref()
.and_then(|t| t.strip_suffix('s'))
.unwrap_or("5");
let retries = hc.retries.unwrap_or(3);
script.push_str(&format!(
"sleep 1\nfor _i in $(seq 1 {retries}); do\n\
\x20 if ! kill -0 \"$FORJAR_SVC_PID\" 2>/dev/null; then\n\
\x20\x20\x20 echo 'service={rid} DIED during startup (pid='$FORJAR_SVC_PID')'\n\
\x20\x20\x20 tail -20 /tmp/forjar-svc-{rid}.log 2>/dev/null || true\n\
\x20\x20\x20 rm -f '{pidfile}'\n\
\x20\x20\x20 exit 1\n\
\x20 fi\n\
\x20 if timeout {timeout} bash -c '{}'; then\n\
\x20\x20\x20 echo 'service={rid} healthy'\n\
\x20\x20\x20 exit 0\n\
\x20 fi\n\
\x20 sleep 1\ndone\n\
echo 'service={rid} started but health check pending'\n",
hc.command
));
}
script
}
fn dispatch_script(resource: &Resource) -> String {
let command = resource.command.as_deref().unwrap_or("true");
let mut script = String::from("set -euo pipefail\n");
if let Some(ref dir) = resource.working_dir {
script.push_str(&format!("cd '{dir}'\n"));
}
if let Some(ref gate) = resource.quality_gate {
if let Some(ref gate_cmd) = gate.command {
let msg = gate
.message
.as_deref()
.unwrap_or("dispatch gate check failed");
script.push_str(&format!(
"if ! bash -c '{gate_cmd}'; then\n\
\x20 echo 'DISPATCH BLOCKED: {msg}'\n\
\x20 exit 1\nfi\n"
));
}
}
if let Some(scatter) = scatter_script(resource) {
script.push_str(&scatter);
}
if let Some(timeout_secs) = resource.timeout {
script.push_str(&format!(
"timeout {timeout_secs} bash <<'FORJAR_TIMEOUT'\n{command}\nFORJAR_TIMEOUT\n"
));
} else {
script.push_str(command);
script.push('\n');
}
if let Some(gather) = gather_script(resource) {
script.push_str(&gather);
}
script
}
pub fn state_query_script(resource: &Resource) -> String {
if !resource.output_artifacts.is_empty() {
let hash_cmds: Vec<String> = resource
.output_artifacts
.iter()
.map(|a| format!("[ -f '{a}' ] && b3sum '{a}' 2>/dev/null || echo 'missing:{a}'"))
.collect();
return hash_cmds.join("\n");
}
let command = resource.command.as_deref().unwrap_or("true");
format!("echo 'command={command}'")
}
pub fn scatter_script(resource: &Resource) -> Option<String> {
if resource.scatter.is_empty() {
return None;
}
let mut script = String::from("set -euo pipefail\n# FJ-2704: scatter artifacts\n");
for mapping in &resource.scatter {
if let Some((local, remote)) = mapping.split_once(':') {
script.push_str(&format!(
"mkdir -p \"$(dirname '{remote}')\"\ncp -r '{local}' '{remote}'\n"
));
}
}
Some(script)
}
pub fn gather_script(resource: &Resource) -> Option<String> {
if resource.gather.is_empty() {
return None;
}
let mut script = String::from("set -euo pipefail\n# FJ-2704: gather artifacts\n");
for mapping in &resource.gather {
if let Some((remote, local)) = mapping.split_once(':') {
script.push_str(&format!(
"mkdir -p \"$(dirname '{local}')\"\ncp -r '{remote}' '{local}'\n"
));
}
}
Some(script)
}