use crate::core::shell_escape::{sh_squote, slugify_identifier};
use crate::core::types::{Resource, TaskMode};
fn service_rid(resource: &Resource) -> String {
slugify_identifier(resource.name.as_deref().unwrap_or("task"))
}
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 = service_rid(resource);
let pidfile = sh_squote(&format!("/tmp/forjar-svc-{rid}.pid"));
let ldd_check = extract_absolute_binary(resource.command.as_deref().unwrap_or(""))
.map(|bin| {
let b = sh_squote(bin);
format!(
"if command -v ldd >/dev/null 2>&1 && [ -f {b} ]; then \
if ldd {b} 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 {} ]", sh_squote(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 {}\n", sh_squote(dir)));
}
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 {}\n",
sh_squote(&format!("=== Stage: {name} ==="))
));
if stage.gate {
script.push_str(&format!(
"if ! bash -c '{cmd}'; then\n echo {}\n exit 1\nfi\n",
sh_squote(&format!("GATE FAILED: {name}"))
));
} 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 {}\n", sh_squote(dir)));
}
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 = service_rid(resource);
let pidfile = sh_squote(&format!("/tmp/forjar-svc-{rid}.pid"));
let logfile = sh_squote(&format!("/tmp/forjar-svc-{rid}.log"));
let mut script = String::from("set -euo pipefail\n");
if let Some(ref dir) = resource.working_dir {
script.push_str(&format!("cd {}\n", sh_squote(dir)));
}
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}' > {logfile} 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 {logfile} 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 {}\n", sh_squote(dir)));
}
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 {}\n\
\x20 exit 1\nfi\n",
sh_squote(&format!("DISPATCH BLOCKED: {msg}"))
));
}
}
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| {
let q = sh_squote(a);
format!(
"[ -f {q} ] && b3sum {q} 2>/dev/null || echo {}",
sh_squote(&format!("missing:{a}"))
)
})
.collect();
return hash_cmds.join("\n");
}
let command = resource.command.as_deref().unwrap_or("true");
format!("echo {}", sh_squote(&format!("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(':') {
let (l, r) = (sh_squote(local), sh_squote(remote));
script.push_str(&format!("mkdir -p \"$(dirname {r})\"\ncp -r {l} {r}\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(':') {
let (r, l) = (sh_squote(remote), sh_squote(local));
script.push_str(&format!("mkdir -p \"$(dirname {l})\"\ncp -r {r} {l}\n"));
}
}
Some(script)
}
#[cfg(test)]
mod fj154_tests {
use super::*;
use crate::core::types::{MachineTarget, ResourceType};
fn service_resource(name: &str) -> Resource {
Resource {
resource_type: ResourceType::Task,
machine: MachineTarget::Single("m1".to_string()),
name: Some(name.to_string()),
command: Some("/usr/bin/myd".to_string()),
task_mode: Some(TaskMode::Service),
..Default::default()
}
}
#[test]
fn fj154_service_name_slugified_in_log_path() {
let r = service_resource("x; rm -rf ~ #");
let script = apply_script(&r);
assert!(
script.contains("> '/tmp/forjar-svc-x--rm--rf.log'"),
"{script}"
);
assert!(!script.contains("/tmp/forjar-svc-x; rm -rf ~"), "{script}");
}
#[test]
fn fj154_service_log_and_pid_paths_consistent() {
let r = service_resource("my svc");
let apply = apply_script(&r);
let check = check_script(&r);
assert!(apply.contains("'/tmp/forjar-svc-my-svc.pid'"), "{apply}");
assert!(check.contains("'/tmp/forjar-svc-my-svc.pid'"), "{check}");
assert!(apply.contains("'/tmp/forjar-svc-my-svc.log'"), "{apply}");
}
#[test]
fn fj154_service_benign_name_unchanged() {
let r = service_resource("web");
let script = apply_script(&r);
assert!(script.contains("> '/tmp/forjar-svc-web.log'"), "{script}");
assert!(script.contains("'/tmp/forjar-svc-web.pid'"), "{script}");
}
#[test]
fn fj154_output_artifacts_quoted() {
let mut r = service_resource("t");
r.task_mode = None;
r.output_artifacts = vec!["/out/x';id;'".to_string()];
let q = state_query_script(&r);
assert!(q.contains("'\\''"), "{q}");
assert!(!q.contains("b3sum '/out/x';id"), "{q}");
}
}