Repository: winstonallo/taskmaster
Pull request #18: Added shell capabilities and fixes
Author: login: fbruggem
Created: 2025-03-30T19:48:25Z | Merged: 2025-03-31T07:41:48Z

For the most part the existing structure was already fitting to what i needed to add.

Also the Ready state was added
And also a check to not have panic's when .stop() is called on a not running process.



Diff:
diff --git a/config/example.toml b/config/example.toml
index 285714b..b92aede 100644
--- a/config/example.toml
+++ b/config/example.toml
@@ -167,4 +167,4 @@ stderr = "/tmp/ls.stderr"
 # Key value pairs of environment variables to be injected into the process
 # at startup.
 # Defaults to [[]].
-env = [["STARTED_BY", "abied-ch"], ["ANSWER", "42"]]
+env = [["STARTED_BY", "abied-ch"], ["ANSWER", "42"]]
\ No newline at end of file
diff --git a/config/felix.toml b/config/felix.toml
new file mode 100644
index 0000000..155977d
--- /dev/null
+++ b/config/felix.toml
@@ -0,0 +1,87 @@
+# Example config for taskmaster.
+
+# Path to the socket used for connections between taskmaster and its client.
+# Defaults to "/tmp/.taskmaster.sock".
+socketpath = "/tmp/.taskmaster.sock"
+
+# Name of the group to be created for authenticating client (similarly to 
+# the docker group.
+# Defaults to "taskmaster".
+authgroup = "everyone"
+
+# Process name.
+[ls]
+
+# Command to run in order to start this process.
+# Must be an absolute path.
+# Required.
+cmd = "/Users/code/source/taskmaster/a.out"
+
+# Arguments to pass to 'cmd'.
+# Defaults to [].
+args = []
+
+# Number of processes to start.
+# Defaults to 1, max 255.
+processes = 100
+
+# Mask for files created by the process.
+# Defaults to "022".
+umask = "022"
+
+# Working directory for the process.
+# Must be an absolute path.
+# Required.
+workingdir = "/tmp"
+
+# Start the program automatically when starting taskmaster. 
+# Defaults to false.
+autostart = true
+
+# Restart the process when it quits, options are:
+# - "no" (default): Never restart the process automatically.
+# - "on-failure[:max-retries]": Try to restart the process 
+#   <max-retries> times when it exits unexpectedly. Maximum value for 
+#   max-retries is 255.
+# - "always": Always restart when exiting, no matter the status.
+autorestart = "on-failure[:5]"
+
+# Backoff time (in seconds) for retries.
+# Defaults to 5, max 255.
+backoff = 1
+
+# List of exit codes to be interpreted as success.
+# Defaults to [0].
+exitcodes = [0, 2]
+
+# Number of attempts to start the program before giving up.
+# Defaults to 3, max 255.
+startretries = 3
+
+# Time (in seconds) given for the program to startup - if the program 
+# does not start within this time, it will be considered a failure.
+# Defaults to 5, max 65536.
+starttime = 1
+
+# List of signals triggering a stop of the process, options are any 
+# FreeBSD signal (https://www.math.stonybrook.edu/~ccc/dfc/dfc/signals.html).
+# Defaults to ["TERM"].
+stopsignals = ["TERM", "USR1"]
+
+# Time (in seconds) to wait for the process to stop. If it does not stop 
+# within this time, it will be forcibly killed.
+# Defaults to 5, max 255.
+stoptime = 5
+
+# File the standard output of the process should be redirected to.
+# Defaults to "/tmp/<process name>.stdout".
+stdout = "/tmp/ls.stdout"
+
+# File the standard error of the process should be redirected to.
+# Defaults to "/tmp/<process name>.stderr".
+stderr = "/tmp/ls.stderr"
+
+# Key value pairs of environment variables to be injected into the process
+# at startup.
+# Defaults to [[]].
+env = [["STARTED_BY", "abied-ch"], ["ANSWER", "42"]]
diff --git a/src/bin/taskmaster.rs b/src/bin/taskmaster.rs
index e547d6f..a7e72f3 100644
--- a/src/bin/taskmaster.rs
+++ b/src/bin/taskmaster.rs
@@ -23,8 +23,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
     };
 
     log_info!("starting taskmaster..");
-    let _ = tasklib::run::daemon::run(&mut daemon.processes, conf.socketpath().to_string(), conf.authgroup().to_string()).await;
-
+    let res = tasklib::run::daemon::run(&mut daemon.processes, conf.socketpath().to_string(), conf.authgroup().to_string()).await;
+    if let Ok(()) = res {
+        return Ok(());
+    }
     tokio::signal::ctrl_c().await?;
 
     Ok(())
diff --git a/src/bin/taskshell.rs b/src/bin/taskshell.rs
index de54535..9ac1684 100644
--- a/src/bin/taskshell.rs
+++ b/src/bin/taskshell.rs
@@ -3,6 +3,7 @@ use std::{
     os::unix::net::UnixStream,
 };
 
+use serde_json::json;
 use tasklib::jsonrpc::JsonRPCRequest;
 
 fn read_from_stream(unix_stream: &mut UnixStream) -> Result<(), String> {
@@ -34,7 +35,9 @@ fn main() {
         jsonrpc: "2.0".to_string(),
         id: 1,
         method: args[1].clone(),
-        params: None,
+        params: Some(json!({
+            // "name": "ls",
+        })),
     })
     .expect("serde failed");
 
diff --git a/src/jsonrpc.rs b/src/jsonrpc.rs
index 7c7b5f0..f404954 100644
--- a/src/jsonrpc.rs
+++ b/src/jsonrpc.rs
@@ -1,3 +1,5 @@
+pub mod handlers;
+
 use serde::{Deserialize, Serialize};
 
 #[repr(i16)]
@@ -29,7 +31,7 @@ impl<'de> Deserialize<'de> for JsonRPCErrorCode {
     }
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct JsonRPCRequest {
     pub jsonrpc: String,
     pub id: usize,
@@ -44,6 +46,16 @@ pub struct JsonRPCResponse {
     pub result: serde_json::Value,
 }
 
+impl JsonRPCResponse {
+    pub fn from_json_rpc_request(request: &JsonRPCRequest, result: serde_json::Value) -> Self {
+        Self {
+            jsonrpc: request.jsonrpc.clone(),
+            id: request.id,
+            result,
+        }
+    }
+}
+
 #[derive(Debug, Serialize, Deserialize)]
 pub struct JsonRPCErrorData {
     pub code: JsonRPCErrorCode,
@@ -58,6 +70,16 @@ pub struct JsonRPCError {
     pub error: JsonRPCErrorData,
 }
 
+impl JsonRPCError {
+    pub fn from_json_rpc_request(request: &JsonRPCRequest, data: JsonRPCErrorData) -> Self {
+        Self {
+            jsonrpc: request.jsonrpc.clone(),
+            id: request.id,
+            error: data,
+        }
+    }
+}
+
 #[derive(Serialize, Deserialize)]
 pub struct JsonRPCRaw {
     pub jsonrpc: String,
@@ -123,7 +145,3 @@ impl TryFrom<JsonRPCRaw> for JsonRPCMessage {
         })
     }
 }
-
-pub trait Method {
-    fn handle(request: JsonRPCRequest) -> Result<JsonRPCResponse, JsonRPCError>;
-}
diff --git a/src/jsonrpc/handlers.rs b/src/jsonrpc/handlers.rs
new file mode 100644
index 0000000..89b684f
--- /dev/null
+++ b/src/jsonrpc/handlers.rs
@@ -0,0 +1,237 @@
+use std::collections::HashMap;
+
+use serde_json::json;
+
+use crate::{
+    conf::Config,
+    run::{
+        self,
+        daemon::Daemon,
+        statemachine::{Process, states::ProcessState},
+    },
+};
+
+use super::{JsonRPCError, JsonRPCErrorCode, JsonRPCErrorData, JsonRPCRequest, JsonRPCResponse};
+
+pub fn handle(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    match request.method.as_str() {
+        "start" => handle_start(request, procs),
+        "stop" => handle_stop(request, procs),
+        "restart" => handle_restart(request, procs),
+        "status" => handle_status(request, procs),
+        "reload" => handle_reload(request, procs),
+        _ => Err(JsonRPCError::from_json_rpc_request(
+            &request,
+            JsonRPCErrorData {
+                code: JsonRPCErrorCode::MethodNotFound,
+                message: format!("method {} not implemented", request.method),
+                data: None,
+            },
+        )),
+    }
+}
+pub fn handle_halt(request: &JsonRPCRequest) -> Option<JsonRPCResponse> {
+    match request.method.as_str() {
+        "halt" => Some(JsonRPCResponse::from_json_rpc_request(request, json!("taskmaster shutting down - goodbye"))),
+        _ => None,
+    }
+}
+
+pub fn get_proc_from_json_request<'a>(
+    request: &JsonRPCRequest,
+    procs: &'a mut HashMap<String, run::statemachine::Process>,
+) -> Result<&'a mut Process, JsonRPCError> {
+    let wrong_params_json_rpc_error = JsonRPCError::from_json_rpc_request(
+        request,
+        JsonRPCErrorData {
+            code: JsonRPCErrorCode::InvalidParams,
+            message: "wrong or no params given | `name`".to_string(),
+            data: request.params.clone(),
+        },
+    );
+    let params = match request.params.clone() {
+        Some(value) => value,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+
+    let object = match params.as_object() {
+        Some(object) => object,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+
+    let name = match object.get("name") {
+        Some(name) => name,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+
+    let name = match name.as_str() {
+        Some(name) => name,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+
+    let proc = match procs.get_mut(&name.to_owned()) {
+        None => return Err(wrong_params_json_rpc_error),
+        Some(p) => p,
+    };
+
+    Ok(proc)
+}
+
+pub fn handle_reload(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    let wrong_params_json_rpc_error = JsonRPCError::from_json_rpc_request(
+        &request,
+        JsonRPCErrorData {
+            code: JsonRPCErrorCode::InvalidParams,
+            message: "you provided wrong params".to_string(),
+            data: request.params.clone(),
+        },
+    );
+    let conf = match Config::from_file("./config/example.toml") {
+        Ok(c) => c,
+        Err(_e) => return Err(wrong_params_json_rpc_error),
+    };
+
+    let daemon = match Daemon::from_config(&conf) {
+        Ok(d) => d,
+        Err(_e) => return Err(wrong_params_json_rpc_error),
+    };
+
+    let mut leftover = vec![];
+    for (name, _p) in procs.iter() {
+        leftover.push(name.to_owned());
+    }
+
+    for (name, p) in daemon.processes {
+        match procs.get_mut(&name) {
+            Some(old_process) => {
+                *old_process.config_mut() = p.config().clone();
+
+                use ProcessState::*;
+                match (p.config().autostart(), old_process.state()) {
+                    (false, Ready | HealthCheck(_) | Healthy) => {
+                        let _ = old_process.stop();
+                        old_process.update_state(Idle);
+                    }
+                    (true, Idle | Completed | Stopped | Failed(_) | WaitingForRetry(_)) => {
+                        old_process.update_state(Ready);
+                    }
+                    _ => {}
+                }
+                if let Some(index) = leftover.iter().position(|n| *n == old_process.name()) {
+                    leftover.remove(index);
+                }
+            }
+            None => {
+                procs.insert(name, p);
+            }
+        }
+    }
+
+    for l in leftover.iter() {
+        if let Some(mut p) = procs.remove_entry(l) {
+            let _ = p.1.stop();
+        }
+    }
+
+    Ok(JsonRPCResponse::from_json_rpc_request(&request, json!("sucessfully reloaded config")))
+}
+
+pub fn handle_status(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    let wrong_params_json_rpc_error = JsonRPCError::from_json_rpc_request(
+        &request,
+        JsonRPCErrorData {
+            code: JsonRPCErrorCode::InvalidParams,
+            message: "wrong or no params given | `name`".to_string(),
+            data: request.params.clone(),
+        },
+    );
+    let params = match request.params.clone() {
+        Some(value) => value,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+
+    let object = match params.as_object() {
+        Some(object) => object,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+    match object.get("name") {
+        None => {
+            let mut line: String = String::new();
+            line.push_str("processes: [");
+            for (_, p) in procs.iter() {
+                line.push_str(&format!("{{name: {}, state: {}}}", p.name(), p.state()));
+            }
+            line.push(']');
+
+            Ok(JsonRPCResponse::from_json_rpc_request(&request, json!(line)))
+        }
+        Some(id) => match id.as_str() {
+            None => Err(wrong_params_json_rpc_error),
+            Some(id) => match procs.get_mut(id) {
+                None => Err(wrong_params_json_rpc_error),
+                Some(p) => Ok(JsonRPCResponse::from_json_rpc_request(
+                    &request,
+                    json!(format!(r#"{{"name": {}, "state": {}}}"#, p.name(), p.state())),
+                )),
+            },
+        },
+    }
+}
+
+pub fn handle_restart(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    match get_proc_from_json_request(&request, procs) {
+        Ok(proc) => {
+            use ProcessState::*;
+            match proc.state().clone() {
+                Idle | WaitingForRetry(_) | Failed(_) | Completed | Stopped => {
+                    proc.update_state(Ready);
+                    Ok(JsonRPCResponse::from_json_rpc_request(&request, json!("process dead - starting process")))
+                }
+                Ready | HealthCheck(_) | Healthy => {
+                    let _ = proc.stop();
+                    proc.update_state(Ready);
+                    Ok(JsonRPCResponse::from_json_rpc_request(
+                        &request,
+                        json!("process running - stopping and starting process"),
+                    ))
+                }
+            }
+        }
+        Err(e) => Err(e),
+    }
+}
+
+pub fn handle_stop(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    match get_proc_from_json_request(&request, procs) {
+        Ok(p) => {
+            use ProcessState::*;
+            match p.state().clone() {
+                Idle | WaitingForRetry(_) | Failed(_) | Completed | Stopped => {
+                    Ok(JsonRPCResponse::from_json_rpc_request(&request, json!("already not running")))
+                }
+                Ready | HealthCheck(_) | Healthy => {
+                    let _ = p.stop();
+                    p.update_state(Stopped);
+                    Ok(JsonRPCResponse::from_json_rpc_request(&request, json!("process running, killing process...")))
+                }
+            }
+        }
+        Err(e) => Err(e),
+    }
+}
+
+pub fn handle_start(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    match get_proc_from_json_request(&request, procs) {
+        Ok(p) => {
+            use ProcessState::*;
+            match p.state().clone() {
+                Idle | WaitingForRetry(_) | Failed(_) | Completed | Stopped => {
+                    p.update_state(Ready);
+                    Ok(JsonRPCResponse::from_json_rpc_request(&request, json!("starting process")))
+                }
+                Ready | HealthCheck(_) | Healthy => Ok(JsonRPCResponse::from_json_rpc_request(&request, json!("already running not starting"))),
+            }
+        }
+        Err(e) => Err(e),
+    }
+}
diff --git a/src/run.rs b/src/run.rs
index eba9df3..8a2c354 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -1,3 +1,3 @@
 pub mod daemon;
 mod proc;
-mod statemachine;
+pub(crate) mod statemachine;
diff --git a/src/run/daemon.rs b/src/run/daemon.rs
index 5dbe6f0..c81c00e 100644
--- a/src/run/daemon.rs
+++ b/src/run/daemon.rs
@@ -1,25 +1,29 @@
-use std::{collections::HashMap, error::Error, time::Duration};
+use std::{collections::HashMap, error::Error, sync::Arc, time::Duration};
 
 use socket::AsyncUnixSocket;
 use tokio::time::sleep;
 
 use super::{
     proc::{self, Process},
-    statemachine,
+    statemachine::{self},
 };
 
-use crate::{conf, log_error};
+use crate::{
+    conf,
+    jsonrpc::{self, JsonRPCRequest},
+    log_error, log_info,
+};
 mod command;
 mod error;
 mod socket;
 
-pub struct Daemon<'tm> {
-    pub processes: HashMap<String, proc::Process<'tm>>,
+pub struct Daemon {
+    pub processes: HashMap<String, proc::Process>,
 }
 
-impl<'tm> Daemon<'tm> {
-    pub fn from_config(conf: &'tm conf::Config) -> Result<Self, Box<dyn Error>> {
-        let procs: HashMap<String, proc::Process<'tm>> = conf
+impl Daemon {
+    pub fn from_config(conf: &conf::Config) -> Result<Self, Box<dyn Error>> {
+        let procs: HashMap<String, proc::Process> = conf
             .processes()
             .iter()
             .flat_map(|(proc_name, proc)| {
@@ -29,10 +33,10 @@ impl<'tm> Daemon<'tm> {
                     } else {
                         proc_name.to_owned()
                     };
-                    (key.clone(), proc::Process::from_process_config(proc, &key))
+                    (key.clone(), proc::Process::from_process_config(proc.clone(), &key))
                 })
             })
-            .collect::<HashMap<String, proc::Process<'tm>>>();
+            .collect::<HashMap<String, proc::Process>>();
 
         Ok(Self { processes: procs })
     }
@@ -48,26 +52,71 @@ pub fn monitor_state(procs: &mut HashMap<String, Process>) {
     }
 }
 
-async fn handle_client(mut socket: AsyncUnixSocket) {
+async fn handle_client(mut socket: AsyncUnixSocket, sender: Arc<tokio::sync::mpsc::Sender<(JsonRPCRequest, AsyncUnixSocket)>>) {
     let mut line = String::new();
     match socket.read_line(&mut line).await {
         Ok(0) => { /* connection closed, do nothing */ }
-        Ok(_) => {
-            println!("{}", line);
-
-            if let Err(e) = socket.write(line.as_bytes()).await {
-                log_error!("error writing to client: {}", e);
+        Ok(_) => match serde_json::from_str(&line) {
+            Ok(request) => {
+                let _ = sender.send((request, socket)).await;
             }
-        }
+            Err(e) => {
+                log_error!("error deserializing request: {}", e)
+            }
+        },
         Err(e) => {
             log_error!("Error reading from socket: {}", e);
         }
     }
 }
 
-pub async fn run(procs: &mut HashMap<String, Process<'_>>, socketpath: String, authgroup: String) -> Result<(), Box<dyn Error>> {
+pub fn handle_json_rpc_request(request: JsonRPCRequest, mut socket: AsyncUnixSocket, procs: &mut HashMap<String, Process>) -> bool {
+    if let Some(resp) = jsonrpc::handlers::handle_halt(&request) {
+        match serde_json::to_string(&resp) {
+            Err(_) => {}
+            Ok(s) => {
+                tokio::spawn(async move {
+                    let _ = socket.write(s.as_bytes()).await;
+                });
+            }
+        }
+
+        for p in procs.iter_mut() {
+            let _ = p.1.stop();
+        }
+        log_info!("shutting down taskmaster...");
+        return true;
+    }
+
+    let res = jsonrpc::handlers::handle(request, procs);
+
+    match res {
+        Ok(resp) => match serde_json::to_string(&resp) {
+            Err(_) => {}
+            Ok(s) => {
+                tokio::spawn(async move {
+                    let _ = socket.write(s.as_bytes()).await;
+                });
+            }
+        },
+        Err(err) => match serde_json::to_string(&err) {
+            Err(_) => {}
+            Ok(s) => {
+                tokio::spawn(async move {
+                    let _ = socket.write(s.as_bytes()).await;
+                });
+            }
+        },
+    };
+    false
+}
+
+pub async fn run(procs: &mut HashMap<String, Process>, socketpath: String, authgroup: String) -> Result<(), Box<dyn Error>> {
     let mut listener = AsyncUnixSocket::new(&socketpath, &authgroup).unwrap();
 
+    let (sender, mut receiver) = tokio::sync::mpsc::channel(1024);
+    let sender = Arc::new(sender);
+
     loop {
         tokio::select! {
             accept_result = listener.accept() => {
@@ -78,12 +127,18 @@ pub async fn run(procs: &mut HashMap<String, Process<'_>>, socketpath: String, a
                 }
 
                 let socket = listener;
+                let clone = sender.clone();
                 tokio::spawn(async move {
-                    handle_client(socket).await;
+                    handle_client(socket, clone).await;
                 });
 
                 listener = AsyncUnixSocket::new(&socketpath, &authgroup)?;
             },
+            Some((request, socket)) = receiver.recv() => {
+                if handle_json_rpc_request(request, socket, procs) {
+                    return Ok(());
+                }
+             },
             _ = sleep(Duration::from_nanos(1)) => {
                 monitor_state(procs);
             }
diff --git a/src/run/daemon/command.rs b/src/run/daemon/command.rs
index 162d1bd..c4d3995 100644
--- a/src/run/daemon/command.rs
+++ b/src/run/daemon/command.rs
@@ -10,7 +10,7 @@ pub enum CommandError {
     NotFound,
 }
 
-impl Daemon<'_> {
+impl Daemon {
     fn status(&self, args: Vec<&str>) -> Result<String, CommandError> {
         let mut response: Vec<String> = vec![];
 
diff --git a/src/run/proc.rs b/src/run/proc.rs
index 8e13619..c5161eb 100644
--- a/src/run/proc.rs
+++ b/src/run/proc.rs
@@ -8,7 +8,7 @@ use std::{
 
 use crate::{
     conf::{self, proc::ProcessConfig},
-    log_error, log_info,
+    log_error, log_info, proc_info,
 };
 pub use error::ProcessError;
 use libc::{c_int, signal, umask};
@@ -19,26 +19,37 @@ mod error;
 
 #[allow(unused)]
 #[derive(Debug)]
-pub struct Process<'tm> {
+pub struct Process {
     id: Option<u32>,
     name: String,
     child: Option<Child>,
-    conf: &'tm ProcessConfig,
+    conf: ProcessConfig,
     startup_failures: u8,
     runtime_failures: u8,
     state: Mutex<ProcessState>,
 }
 
-impl<'tm> Process<'tm> {
-    pub fn from_process_config(conf: &'tm conf::proc::ProcessConfig, proc_name: &str) -> Self {
-        Self {
-            id: None,
-            name: proc_name.to_string(),
-            child: None,
-            conf,
-            startup_failures: 0,
-            runtime_failures: 0,
-            state: Mutex::new(ProcessState::Idle),
+impl Process {
+    pub fn from_process_config(conf: conf::proc::ProcessConfig, proc_name: &str) -> Self {
+        match conf.autostart() {
+            true => Self {
+                id: None,
+                name: proc_name.to_string(),
+                child: None,
+                conf,
+                startup_failures: 0,
+                runtime_failures: 0,
+                state: Mutex::new(ProcessState::Ready),
+            },
+            false => Self {
+                id: None,
+                name: proc_name.to_string(),
+                child: None,
+                conf,
+                startup_failures: 0,
+                runtime_failures: 0,
+                state: Mutex::new(ProcessState::Idle),
+            },
         }
     }
 }
@@ -50,7 +61,7 @@ extern "C" fn kill(_signum: c_int) {
 }
 
 #[allow(unused)]
-impl Process<'_> {
+impl Process {
     pub fn state(&self) -> ProcessState {
         self.state.lock().expect("something went terribly wrong").clone()
     }
@@ -69,7 +80,11 @@ impl Process<'_> {
     }
 
     pub fn config(&self) -> &ProcessConfig {
-        self.conf
+        &self.conf
+    }
+
+    pub fn config_mut(&mut self) -> &mut ProcessConfig {
+        &mut self.conf
     }
 
     pub fn runtime_failures(&self) -> u8 {
@@ -121,7 +136,8 @@ impl Process<'_> {
                     for sig in &stop_signals {
                         signal(sig.signal(), kill as usize);
                     }
-                    umask(umask_val as u16);
+                    umask(umask_val);
+
                     Ok(())
                 })
                 .spawn()
@@ -144,12 +160,13 @@ impl Process<'_> {
         Ok(())
     }
 
-    fn check_signal(&self, status: ExitStatus, pid: u32) -> Option<i32> {
+    fn check_signal(&mut self, status: ExitStatus, pid: u32) -> Option<i32> {
         if let Some(signal) = status.signal() {
             log_info!("PID {} terminated by signal {}", pid, signal);
         } else {
             log_info!("PID {} terminated without exit or signal information", pid)
         }
+        self.update_state(ProcessState::Stopped);
         None
     }
 
@@ -178,10 +195,20 @@ impl Process<'_> {
     }
 
     pub fn stop(&mut self) -> std::io::Result<()> {
-        self.child.take().unwrap().kill();
-        self.id.take();
-
-        Ok(())
+        use ProcessState::*;
+        match self.state() {
+            HealthCheck(_) | Healthy => {
+                self.child.take().unwrap().kill();
+                proc_info!(
+                    self.name(),
+                    "killed, PID {}",
+                    self.id().expect("process without id killed - this should not happen")
+                );
+                self.id.take();
+                Ok(())
+            }
+            _ => Ok(()),
+        }
     }
 }
 
@@ -195,7 +222,7 @@ mod tests {
             id: Some(1),
             name: ("".to_string()),
             child: None,
-            conf: &mut conf::proc::ProcessConfig::testconfig(),
+            conf: conf::proc::ProcessConfig::testconfig(),
             startup_failures: 0,
             runtime_failures: 0,
             state: Mutex::new(ProcessState::Idle),
@@ -209,7 +236,7 @@ mod tests {
             id: Some(1),
             name: ("".to_string()),
             child: None,
-            conf: &mut conf::proc::ProcessConfig::testconfig(),
+            conf: conf::proc::ProcessConfig::testconfig(),
             startup_failures: 0,
             runtime_failures: 0,
             state: Mutex::new(ProcessState::Idle),
diff --git a/src/run/statemachine.rs b/src/run/statemachine.rs
index ba6cb2b..93e7437 100644
--- a/src/run/statemachine.rs
+++ b/src/run/statemachine.rs
@@ -1,6 +1,6 @@
-use states::{Completed, Failed, HealthCheck, Healthy, Idle, ProcessState, State, Stopped, WaitingForRetry};
+use states::{Completed, Failed, HealthCheck, Healthy, Idle, ProcessState, Ready, State, Stopped, WaitingForRetry};
 
-use super::proc::Process;
+pub use super::proc::Process;
 
 pub mod states;
 mod transitions;
@@ -16,6 +16,7 @@ fn try_update_state<S: State>(proc: &mut Process, handler: S) {
 pub fn monitor_state(proc: &mut Process) {
     match proc.state() {
         ProcessState::Idle => try_update_state(proc, Idle),
+        ProcessState::Ready => try_update_state(proc, Ready),
         ProcessState::HealthCheck(started_at) => try_update_state(proc, HealthCheck::new(started_at)),
         ProcessState::Healthy => try_update_state(proc, Healthy),
         ProcessState::Failed(prev_state) => try_update_state(proc, Failed::new(*prev_state)),
diff --git a/src/run/statemachine/states.rs b/src/run/statemachine/states.rs
index a20e5e2..f7597d6 100644
--- a/src/run/statemachine/states.rs
+++ b/src/run/statemachine/states.rs
@@ -9,6 +9,7 @@ use crate::run::proc::Process;
 #[derive(Clone, Debug, PartialEq)]
 pub enum ProcessState {
     Idle,
+    Ready,
     HealthCheck(time::Instant),
     Healthy,
     Failed(Box<ProcessState>),
@@ -21,6 +22,7 @@ impl Display for ProcessState {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             ProcessState::Idle => write!(f, "idle"),
+            ProcessState::Ready => write!(f, "ready"),
             ProcessState::HealthCheck(started_at) => write!(f, "in healthcheck since {} seconds", Instant::now().duration_since(*started_at).as_secs()),
             ProcessState::Healthy => write!(f, "healthy"),
             ProcessState::Failed(prev_state) => write!(f, "failed while in state: {}", *prev_state),
@@ -40,6 +42,8 @@ pub trait State {
 
 pub struct Idle;
 
+pub struct Ready;
+
 pub struct HealthCheck {
     started_at: time::Instant,
 }
diff --git a/src/run/statemachine/transitions.rs b/src/run/statemachine/transitions.rs
index e6c927d..fd37762 100644
--- a/src/run/statemachine/transitions.rs
+++ b/src/run/statemachine/transitions.rs
@@ -5,14 +5,16 @@ use crate::{
     run::{proc::Process, statemachine::states::ProcessState},
 };
 
-use super::states::{Completed, Failed, HealthCheck, Healthy, Idle, State, Stopped, WaitingForRetry};
+use super::states::{Completed, Failed, HealthCheck, Healthy, Idle, Ready, State, Stopped, WaitingForRetry};
 
 impl State for Idle {
-    fn handle(&self, p: &mut Process) -> Option<ProcessState> {
-        if !p.config().autostart() {
-            return None;
-        }
+    fn handle(&self, _p: &mut Process) -> Option<ProcessState> {
+        None
+    }
+}
 
+impl State for Ready {
+    fn handle(&self, p: &mut Process) -> Option<ProcessState> {
         match p.start() {
             Ok(()) => {
                 let pid = p.id().expect("id should always be set if the process is running");


Reviews:
user: login: winstonallo
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-30T20:35:38Z
user: login: fbruggem
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-31T06:14:25Z
user: login: fbruggem
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-31T06:58:15Z
user: login: fbruggem
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-31T06:58:52Z
user: login: winstonallo
body: ☠️ 
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-31T07:40:33Z

Review comments:
path: config/example.toml
user: login: winstonallo
body: I would like to keep a full example config in example.toml, if you need one with different features please create a new one
created_at: 2025-03-30T20:21:44Z
author_association: OWNER
path: src/jsonrpc.rs
user: login: winstonallo
body: The jsonrpc module is more of a transport layer detail, maybe we should have the handlers in a different module?
created_at: 2025-03-30T20:24:45Z
author_association: OWNER
diff_hunk: @@ -48,26 +52,36 @@ pub fn monitor_state(procs: &mut HashMap<String, Process>) {
     }
 }
 
-async fn handle_client(mut socket: AsyncUnixSocket) {
+async fn handle_client(mut socket: AsyncUnixSocket, sender: Arc<tokio::sync::mpsc::Sender<(JsonRPCRequest, AsyncUnixSocket)>>) {
     let mut line = String::new();
     match socket.read_line(&mut line).await {
         Ok(0) => { /* connection closed, do nothing */ }
         Ok(_) => {
-            println!("{}", line);
-
-            if let Err(e) = socket.write(line.as_bytes()).await {
-                log_error!("error writing to client: {}", e);
+            match serde_json::from_str(&line) {
+                Ok(request) => {
+                    let _ = sender.send((request, socket)).await;
+                }
+                Err(e) => {
+                    log_error!("error deserializing request: {}", e)
+                }
             }
+
+            // if let Err(e) = socket.write(line.as_bytes()).await {
path: src/run/daemon.rs
user: login: winstonallo
body: Is that commented out code still needed or can we remove it?
created_at: 2025-03-30T20:30:21Z
author_association: OWNER
diff_hunk: @@ -48,26 +52,36 @@ pub fn monitor_state(procs: &mut HashMap<String, Process>) {
     }
 }
 
-async fn handle_client(mut socket: AsyncUnixSocket) {
+async fn handle_client(mut socket: AsyncUnixSocket, sender: Arc<tokio::sync::mpsc::Sender<(JsonRPCRequest, AsyncUnixSocket)>>) {
     let mut line = String::new();
     match socket.read_line(&mut line).await {
         Ok(0) => { /* connection closed, do nothing */ }
         Ok(_) => {
-            println!("{}", line);
-
-            if let Err(e) = socket.write(line.as_bytes()).await {
-                log_error!("error writing to client: {}", e);
+            match serde_json::from_str(&line) {
+                Ok(request) => {
+                    let _ = sender.send((request, socket)).await;
+                }
+                Err(e) => {
+                    log_error!("error deserializing request: {}", e)
+                }
             }
+
+            // if let Err(e) = socket.write(line.as_bytes()).await {
+            //     log_error!("error writing to client: {}", e);
+            // }
         }
         Err(e) => {
             log_error!("Error reading from socket: {}", e);
         }
     }
 }
 
-pub async fn run(procs: &mut HashMap<String, Process<'_>>, socketpath: String, authgroup: String) -> Result<(), Box<dyn Error>> {
+pub async fn run(procs: &mut HashMap<String, Process>, socketpath: String, authgroup: String) -> Result<(), Box<dyn Error>> {
     let mut listener = AsyncUnixSocket::new(&socketpath, &authgroup).unwrap();
 
+    let (sender, mut reciever) = tokio::sync::mpsc::channel(1024);
path: src/run/daemon.rs
user: login: winstonallo
body: Typo in reciever
created_at: 2025-03-30T20:31:12Z
author_association: OWNER
diff_hunk: @@ -48,26 +52,36 @@ pub fn monitor_state(procs: &mut HashMap<String, Process>) {
     }
 }
 
-async fn handle_client(mut socket: AsyncUnixSocket) {
+async fn handle_client(mut socket: AsyncUnixSocket, sender: Arc<tokio::sync::mpsc::Sender<(JsonRPCRequest, AsyncUnixSocket)>>) {
     let mut line = String::new();
     match socket.read_line(&mut line).await {
         Ok(0) => { /* connection closed, do nothing */ }
         Ok(_) => {
-            println!("{}", line);
-
-            if let Err(e) = socket.write(line.as_bytes()).await {
-                log_error!("error writing to client: {}", e);
+            match serde_json::from_str(&line) {
+                Ok(request) => {
+                    let _ = sender.send((request, socket)).await;
+                }
+                Err(e) => {
+                    log_error!("error deserializing request: {}", e)
+                }
             }
+
+            // if let Err(e) = socket.write(line.as_bytes()).await {
+            //     log_error!("error writing to client: {}", e);
+            // }
         }
         Err(e) => {
             log_error!("Error reading from socket: {}", e);
         }
     }
 }
 
-pub async fn run(procs: &mut HashMap<String, Process<'_>>, socketpath: String, authgroup: String) -> Result<(), Box<dyn Error>> {
+pub async fn run(procs: &mut HashMap<String, Process>, socketpath: String, authgroup: String) -> Result<(), Box<dyn Error>> {
path: src/run/daemon.rs
user: login: winstonallo
body: Can we move the client handling and request processing logic to new fuctions to make the daemon::run a bit easier a bit easier to follow?
created_at: 2025-03-30T20:33:50Z
author_association: OWNER
diff_hunk: @@ -124,6 +152,255 @@ impl TryFrom<JsonRPCRaw> for JsonRPCMessage {
     }
 }
 
-pub trait Method {
-    fn handle(request: JsonRPCRequest) -> Result<JsonRPCResponse, JsonRPCError>;
+pub fn handle(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    match request.method.as_str() {
+        "start" => handle_start(request, procs),
+        "stop" => handle_stop(request, procs),
+        "restart" => handle_restart(request, procs),
+        "status" => handle_status(request, procs),
+        "reload" => handle_reload(request, procs),
+        _ => Err(JsonRPCError::from_json_rpc_request(
+            &request,
+            JsonRPCErrorData {
+                code: JsonRPCErrorCode::MethodNotFound,
+                message: format!("method {} not implemented", request.method),
+                data: None,
+            },
+        )),
+    }
+}
+pub fn handle_halt(request: &JsonRPCRequest) -> Option<JsonRPCResponse> {
+    match request.method.as_str() {
+        "halt" => Some(JsonRPCResponse::from_json_rpc_request(request, json!("taskmaster shutting down - goodbye"))),
+        _ => None,
+    }
+}
+
+pub fn handle_reload(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    let wrong_params_json_rpc_error = JsonRPCError::from_json_rpc_request(
+        &request,
+        JsonRPCErrorData {
+            code: JsonRPCErrorCode::InvalidParams,
+            message: "you provided wrong params".to_string(),
+            data: request.params.clone(),
+        },
+    );
+    let conf = match Config::from_file("./config/example.toml") {
+        Ok(c) => c,
+        Err(_e) => return Err(wrong_params_json_rpc_error),
+    };
+
+    let daemon = match Daemon::from_config(&conf) {
+        Ok(d) => d,
+        Err(_e) => return Err(wrong_params_json_rpc_error),
+    };
+
+    let mut leftover = vec![];
+    for (name, _p) in procs.iter() {
+        leftover.push(name.to_owned());
+    }
+
+    for (name, p) in daemon.processes {
+        match procs.get_mut(&name) {
+            Some(old_process) => {
+                *old_process.config_mut() = p.config().clone();
+
+                use ProcessState::*;
+                match (p.config().autostart(), old_process.state()) {
+                    (false, Ready | HealthCheck(_) | Healthy) => {
+                        let _ = old_process.stop();
+                        old_process.update_state(Idle);
+                    }
+                    (true, Idle | Completed | Stopped | Failed(_) | WaitingForRetry(_)) => {
+                        old_process.update_state(Ready);
+                    }
+                    _ => {}
+                }
+                if let Some(index) = leftover.iter().position(|n| *n == old_process.name()) {
+                    leftover.remove(index);
+                }
+            }
+            None => {
+                procs.insert(name, p);
+            }
+        }
+    }
+
+    for l in leftover.iter() {
+        if let Some(mut p) = procs.remove_entry(l) {
+            let _ = p.1.stop();
+        }
+    }
+
+    Ok(JsonRPCResponse::from_json_rpc_request(&request, json!("sucessfully reloaded config")))
+}
+
+pub fn handle_status(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    let wrong_params_json_rpc_error = JsonRPCError::from_json_rpc_request(
+        &request,
+        JsonRPCErrorData {
+            code: JsonRPCErrorCode::InvalidParams,
+            message: "wrong or no params given | `name`".to_string(),
+            data: request.params.clone(),
+        },
+    );
+    let params = match request.params.clone() {
+        Some(value) => value,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+
+    let object = match params.as_object() {
+        Some(object) => object,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+    match object.get("name") {
+        None => {
+            let mut line: String = String::new();
+            line.push_str("processes: [");
+            for (_, p) in procs.iter() {
+                line.push_str(&format!("{{name: {}, state: {}}}", p.name(), p.state()));
+            }
+            line.push(']');
+
+            Ok(JsonRPCResponse::from_json_rpc_request(&request, json!(line)))
+        }
+        Some(id) => match id.as_str() {
+            None => Err(wrong_params_json_rpc_error),
+            Some(id) => match procs.get_mut(id) {
+                None => Err(wrong_params_json_rpc_error),
+                Some(p) => Ok(JsonRPCResponse::from_json_rpc_request(
+                    &request,
+                    json!(format!(r#"{{"name": {}, "state": {}}}"#, p.name(), p.state())),
+                )),
+            },
+        },
+    }
+}
+
+pub fn handle_restart(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    let wrong_params_json_rpc_error = JsonRPCError::from_json_rpc_request(
+        &request,
+        JsonRPCErrorData {
+            code: JsonRPCErrorCode::InvalidParams,
+            message: "wrong or no params given | `name`".to_string(),
+            data: request.params.clone(),
+        },
+    );
+    let params = match request.params.clone() {
+        Some(value) => value,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+
+    let object = match params.as_object() {
+        Some(object) => object,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+    match object.get("name") {
path: src/jsonrpc.rs
user: login: winstonallo
body: Here as well, maybe there is a way to write this without indenting match statements like that, it's a bit hard to follow - not a fan of `tmp` as a variable name either
created_at: 2025-03-30T20:35:32Z
author_association: OWNER
diff_hunk: @@ -48,26 +52,36 @@ pub fn monitor_state(procs: &mut HashMap<String, Process>) {
     }
 }
 
-async fn handle_client(mut socket: AsyncUnixSocket) {
+async fn handle_client(mut socket: AsyncUnixSocket, sender: Arc<tokio::sync::mpsc::Sender<(JsonRPCRequest, AsyncUnixSocket)>>) {
     let mut line = String::new();
     match socket.read_line(&mut line).await {
         Ok(0) => { /* connection closed, do nothing */ }
         Ok(_) => {
-            println!("{}", line);
-
-            if let Err(e) = socket.write(line.as_bytes()).await {
-                log_error!("error writing to client: {}", e);
+            match serde_json::from_str(&line) {
+                Ok(request) => {
+                    let _ = sender.send((request, socket)).await;
+                }
+                Err(e) => {
+                    log_error!("error deserializing request: {}", e)
+                }
             }
+
+            // if let Err(e) = socket.write(line.as_bytes()).await {
path: src/run/daemon.rs
user: login: fbruggem
body: removed
created_at: 2025-03-31T06:14:25Z
author_association: COLLABORATOR
path: src/jsonrpc.rs
user: login: fbruggem
body: made into jsonrpc module
created_at: 2025-03-31T06:58:15Z
author_association: COLLABORATOR
diff_hunk: @@ -124,6 +152,255 @@ impl TryFrom<JsonRPCRaw> for JsonRPCMessage {
     }
 }
 
-pub trait Method {
-    fn handle(request: JsonRPCRequest) -> Result<JsonRPCResponse, JsonRPCError>;
+pub fn handle(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    match request.method.as_str() {
+        "start" => handle_start(request, procs),
+        "stop" => handle_stop(request, procs),
+        "restart" => handle_restart(request, procs),
+        "status" => handle_status(request, procs),
+        "reload" => handle_reload(request, procs),
+        _ => Err(JsonRPCError::from_json_rpc_request(
+            &request,
+            JsonRPCErrorData {
+                code: JsonRPCErrorCode::MethodNotFound,
+                message: format!("method {} not implemented", request.method),
+                data: None,
+            },
+        )),
+    }
+}
+pub fn handle_halt(request: &JsonRPCRequest) -> Option<JsonRPCResponse> {
+    match request.method.as_str() {
+        "halt" => Some(JsonRPCResponse::from_json_rpc_request(request, json!("taskmaster shutting down - goodbye"))),
+        _ => None,
+    }
+}
+
+pub fn handle_reload(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    let wrong_params_json_rpc_error = JsonRPCError::from_json_rpc_request(
+        &request,
+        JsonRPCErrorData {
+            code: JsonRPCErrorCode::InvalidParams,
+            message: "you provided wrong params".to_string(),
+            data: request.params.clone(),
+        },
+    );
+    let conf = match Config::from_file("./config/example.toml") {
+        Ok(c) => c,
+        Err(_e) => return Err(wrong_params_json_rpc_error),
+    };
+
+    let daemon = match Daemon::from_config(&conf) {
+        Ok(d) => d,
+        Err(_e) => return Err(wrong_params_json_rpc_error),
+    };
+
+    let mut leftover = vec![];
+    for (name, _p) in procs.iter() {
+        leftover.push(name.to_owned());
+    }
+
+    for (name, p) in daemon.processes {
+        match procs.get_mut(&name) {
+            Some(old_process) => {
+                *old_process.config_mut() = p.config().clone();
+
+                use ProcessState::*;
+                match (p.config().autostart(), old_process.state()) {
+                    (false, Ready | HealthCheck(_) | Healthy) => {
+                        let _ = old_process.stop();
+                        old_process.update_state(Idle);
+                    }
+                    (true, Idle | Completed | Stopped | Failed(_) | WaitingForRetry(_)) => {
+                        old_process.update_state(Ready);
+                    }
+                    _ => {}
+                }
+                if let Some(index) = leftover.iter().position(|n| *n == old_process.name()) {
+                    leftover.remove(index);
+                }
+            }
+            None => {
+                procs.insert(name, p);
+            }
+        }
+    }
+
+    for l in leftover.iter() {
+        if let Some(mut p) = procs.remove_entry(l) {
+            let _ = p.1.stop();
+        }
+    }
+
+    Ok(JsonRPCResponse::from_json_rpc_request(&request, json!("sucessfully reloaded config")))
+}
+
+pub fn handle_status(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    let wrong_params_json_rpc_error = JsonRPCError::from_json_rpc_request(
+        &request,
+        JsonRPCErrorData {
+            code: JsonRPCErrorCode::InvalidParams,
+            message: "wrong or no params given | `name`".to_string(),
+            data: request.params.clone(),
+        },
+    );
+    let params = match request.params.clone() {
+        Some(value) => value,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+
+    let object = match params.as_object() {
+        Some(object) => object,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+    match object.get("name") {
+        None => {
+            let mut line: String = String::new();
+            line.push_str("processes: [");
+            for (_, p) in procs.iter() {
+                line.push_str(&format!("{{name: {}, state: {}}}", p.name(), p.state()));
+            }
+            line.push(']');
+
+            Ok(JsonRPCResponse::from_json_rpc_request(&request, json!(line)))
+        }
+        Some(id) => match id.as_str() {
+            None => Err(wrong_params_json_rpc_error),
+            Some(id) => match procs.get_mut(id) {
+                None => Err(wrong_params_json_rpc_error),
+                Some(p) => Ok(JsonRPCResponse::from_json_rpc_request(
+                    &request,
+                    json!(format!(r#"{{"name": {}, "state": {}}}"#, p.name(), p.state())),
+                )),
+            },
+        },
+    }
+}
+
+pub fn handle_restart(request: JsonRPCRequest, procs: &mut HashMap<String, run::statemachine::Process>) -> Result<JsonRPCResponse, JsonRPCError> {
+    let wrong_params_json_rpc_error = JsonRPCError::from_json_rpc_request(
+        &request,
+        JsonRPCErrorData {
+            code: JsonRPCErrorCode::InvalidParams,
+            message: "wrong or no params given | `name`".to_string(),
+            data: request.params.clone(),
+        },
+    );
+    let params = match request.params.clone() {
+        Some(value) => value,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+
+    let object = match params.as_object() {
+        Some(object) => object,
+        None => return Err(wrong_params_json_rpc_error),
+    };
+    match object.get("name") {
path: src/jsonrpc.rs
user: login: fbruggem
body: had to do that before i remove the <'_> lifetime from Process. Now it could be removed without a problem without the lifetime restrictions.
created_at: 2025-03-31T06:58:52Z
author_association: COLLABORATOR

PR comments:
user: login: fbruggem
created_at: 2025-03-30T20:13:33Z
body: Btw the cargo tests are also failing on main rn. IDK why tho
author_association: COLLABORATOR
user: login: fbruggem
created_at: 2025-03-30T20:14:50Z
body: also the clippy tests are failing but I made sure that I `git pull` `git status` and run clippy everything is good. But on the pipline it complains...
<img width="2056" alt="image" src="https://github.com/user-attachments/assets/bb23b90e-93c7-400f-bfde-d09f12598fb4" />

author_association: COLLABORATOR
user: login: winstonallo
created_at: 2025-03-30T20:29:05Z
body: > also the clippy tests are failing but I made sure that I `git pull` `git status` and run clippy everything is good. But on the pipline it complains...

try `rustup update`, for me it also fails locally

author_association: OWNER
user: login: winstonallo
created_at: 2025-03-30T20:43:09Z
body: Thanks! 
The code is a bit hard to follow in some places - this could use a bit of cleaning up imo.  I don't believe that the command handlers have anything to do in the `jsonrpc` module. Some functions are very big and have up to 8 levels of indentation, would be nice to rework them a little for our future selves' sake. 

The failing tests passing on your machine are probably due to your local rustup version not being up to date, here is the cargo test output on my machine:
```sh
➜  taskmaster git:(14-shell-capabilities) cargo test
   Compiling taskmaster v0.1.0 (/home/winstonallo/mastery/taskmaster)
error[E0308]: mismatched types
    --> src/run/proc.rs:139:27
     |
139  |                     umask(umask_val);
     |                     ----- ^^^^^^^^^ expected `u32`, found `u16`
     |                     |
     |                     arguments to this function are incorrect
     |
note: function defined here
    --> /home/winstonallo/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.169/src/unix/mod.rs:1063:12
     |
1063 |     pub fn umask(mask: mode_t) -> mode_t;
     |            ^^^^^
help: you can convert a `u16` to a `u32`
     |
139  |                     umask(umask_val.into());
     |                                    +++++++

For more information about this error, try `rustc --explain E0308`.
error: could not compile `taskmaster` (lib) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
error: could not compile `taskmaster` (lib test) due to 1 previous error
```
author_association: OWNER
user: login: winstonallo
created_at: 2025-03-31T07:05:12Z
body: Ok just 2 last things before we merge: 
- [ ] fix CI
- [x] modern convention for module definition is to not use jsonrpc/mod.rs but rather jsonrpc.rs and the rest of the module files in the jsonrpc/ directory https://stackoverflow.com/questions/26435102/in-rust-what-is-the-purpose-of-a-mod-rs-file#:~:text=From%20the%20Rust%20reference%3A,mod.rs%20within%20a%20project.
author_association: OWNER
user: login: fbruggem
created_at: 2025-03-31T07:17:19Z
body: > > also the clippy tests are failing but I made sure that I `git pull` `git status` and run clippy everything is good. But on the pipline it complains...
> 
> try `rustup update`, for me it also fails locally

But the function wants a u16...

<img width="860" alt="image" src="https://github.com/user-attachments/assets/c85ab01b-78be-4380-986f-bf1469271398" />
author_association: COLLABORATOR
user: login: fbruggem
created_at: 2025-03-31T07:41:19Z
body: @winstonallo  Ok so my local install of libc expects a u16 but the one in the pipeline expects a u32. 

So I currently can't run the code locally but CI is happy - peace out!
author_association: COLLABORATOR