Skip to main content

mj_controller/targets/
worker_daemon.rs

1use super::*;
2
3/// POSIX shell helpers that identify the daemon for one exact worker root.
4/// The match is assembled at run time so the script's own command line cannot
5/// select itself, and `worker proxy` command lines cannot match either.
6pub fn worker_daemon_identity_script(worker_root: &str) -> String {
7    format!(
8        r#"hel_root={root}
9hel_match="hel worker run --root $hel_root"
10hel_match_home="hel worker run --root $HOME/$hel_root"
11hel_ps() {{
12    ps -ww "$@" 2>/dev/null || ps "$@" 2>/dev/null
13}}
14hel_is_worker() {{
15    hel_args=$(hel_ps -o args= -p "$1") || return 1
16    case "$hel_args" in
17        *"$hel_match"*|*"$hel_match_home"*) return 0 ;;
18    esac
19    return 1
20}}
21hel_recorded_worker() {{
22    [ -f "$hel_root/{pid_file}" ] || return 1
23    hel_pid=$(cat "$hel_root/{pid_file}" 2>/dev/null)
24    case "$hel_pid" in
25        '' | *[!0-9]*) return 1 ;;
26    esac
27    hel_is_worker "$hel_pid" || return 1
28    printf '%s\n' "$hel_pid"
29}}"#,
30        root = posix_quote(worker_root),
31        pid_file = mj_core::relay::WORKER_PID_FILE,
32    )
33}
34
35/// Report whether the exact session worker is alive without signaling it.
36/// A successful probe prints one stable token; transport or shell failures
37/// stay distinguishable from a confirmed absent worker.
38pub fn worker_daemon_liveness_script(worker_root: &str) -> String {
39    let mut script = worker_daemon_identity_script(worker_root);
40    script.push_str(
41        r#"
42hel_report_worker_state() {
43    if [ -S "$hel_root/control.sock" ]; then
44        printf 'alive\n'
45    else
46        printf 'starting\n'
47    fi
48}
49if hel_recorded_worker >/dev/null; then
50    hel_report_worker_state
51    exit 0
52fi
53while read -r hel_pid hel_args; do
54    case "$hel_pid" in
55        '' | *[!0-9]*) continue ;;
56    esac
57    [ "$hel_pid" -eq $$ ] && continue
58    case "$hel_args" in
59        *"$hel_match"*|*"$hel_match_home"*) hel_report_worker_state; exit 0 ;;
60    esac
61done <<MJ_PS
62$(hel_ps -eo pid=,args=)
63MJ_PS
64printf 'dead\n'
65"#,
66    );
67    script
68}
69
70/// Stop the detached worker daemon rooted at `worker_root`.
71///
72/// The daemon leads its own process group, so the signal goes to the group
73/// first to take the agent down with it. Shells disagree about how to write a
74/// negative PID (`dash` rejects `--`), hence the two forms before the
75/// single-process fallback for daemons predating the group leadership.
76pub fn stop_worker_daemon_script(worker_root: &str) -> String {
77    let mut script = worker_daemon_identity_script(worker_root);
78    script.push_str(
79        r#"
80hel_signal() {
81    kill -"$1" -- "-$2" 2>/dev/null && return 0
82    kill -"$1" "-$2" 2>/dev/null && return 0
83    kill -"$1" "$2" 2>/dev/null
84}
85hel_stop() {
86    hel_signal TERM "$1" || return 0
87    hel_waited=0
88    while [ "$hel_waited" -lt 2 ]; do
89        kill -0 "$1" 2>/dev/null || return 0
90        sleep 1
91        hel_waited=$((hel_waited + 1))
92    done
93    kill -0 "$1" 2>/dev/null || return 0
94    hel_signal KILL "$1" || true
95    hel_waited=0
96    while [ "$hel_waited" -lt 3 ]; do
97        kill -0 "$1" 2>/dev/null || return 0
98        sleep 1
99        hel_waited=$((hel_waited + 1))
100    done
101}
102if hel_pid=$(hel_recorded_worker); then
103    hel_stop "$hel_pid"
104fi
105hel_ps -eo pid=,args= | while read -r hel_pid hel_args; do
106    case "$hel_pid" in
107        '' | *[!0-9]*) continue ;;
108    esac
109    [ "$hel_pid" -eq $$ ] && continue
110    case "$hel_args" in
111        *"$hel_match"*|*"$hel_match_home"*) hel_stop "$hel_pid" ;;
112    esac
113done
114hel_left=0
115while read -r hel_pid hel_args; do
116    case "$hel_pid" in
117        '' | *[!0-9]*) continue ;;
118    esac
119    [ "$hel_pid" -eq $$ ] && continue
120    case "$hel_args" in
121        *"$hel_match"*|*"$hel_match_home"*) hel_left=1 ;;
122    esac
123done <<MJ_PS
124$(hel_ps -eo pid=,args=)
125MJ_PS
126if [ "$hel_left" -ne 0 ]; then
127    echo "worker still running after stop: $hel_root" >&2
128    exit 1
129fi
130"#,
131    );
132    script
133}
134
135/// Stop a leaked worker and delete the durable relay state under its root.
136///
137/// A resume seeds fresh relay state into the same root a closed session used.
138/// Leftover state wins over that seed at startup, so it has to go, and
139/// whatever might still be writing it has to go first. Container and instance
140/// targets are rebuilt from scratch on resume, so they need nothing here.
141pub fn clear_relay_state_plan(
142    locator: &TargetLocator,
143    session_id: &str,
144) -> Result<Option<CommandSpec>> {
145    verify_locator(locator, session_id)?;
146    let session_worker_root = worker_root(locator, session_id)?;
147    let script = format!(
148        "{}\nrm -rf -- {} {}\n",
149        stop_worker_daemon_script(&session_worker_root),
150        posix_quote(&format!(
151            "{session_worker_root}/{}",
152            mj_core::relay::RELAY_STATE_FILE
153        )),
154        posix_quote(&format!(
155            "{session_worker_root}/{}",
156            mj_core::relay::RELAY_JOURNAL_DIR
157        )),
158    );
159    Ok(match locator {
160        TargetLocator::LocalBare { .. } => Some(
161            CommandSpec::new("sh", ["-c", script.as_str()])
162                .purpose("stop a leaked local Mjolnir worker and clear its relay state"),
163        ),
164        TargetLocator::SshBare { ssh, .. } => Some(
165            ssh_command(ssh, ["sh", "-c", script.as_str()])
166                .purpose("stop a leaked remote Mjolnir worker and clear its relay state"),
167        ),
168        TargetLocator::LocalPodman { .. }
169        | TargetLocator::LocalDocker { .. }
170        | TargetLocator::AppleContainer { .. }
171        | TargetLocator::SshPodman { .. }
172        | TargetLocator::SshDocker { .. }
173        | TargetLocator::AwsEc2 { .. } => None,
174    })
175}