+++ title = "Thrifty Rubidium" date = 2022-06-14 [taxonomies] tags = ["projects", "personal", "make", "runner"] +++ Task Runner. The basic idea is to have a bunch of tasks, and each task have a step. Based on the parallelism available (number of cores, for example), the system would launch each step on each task as long as there is available processing power. Let's start with the definition of task: A task could be, very simply, a directory with a runner configuration file, where the steps are defined. For example: ```json { "name": "ExampleTask", "steps": [ { "name": "Get List", "command": "ls > file.ls" }, { "name": "Find file", "command": "grep somename file.ls" }, { "name": "Remove file", "command": "rm file.ls" } ] } ``` If one of the steps fails, the whole task stops. For example, if grep returns an error code, "Rmeove file" would not be run. {% note() %} **Thought**: Maybe we could add a flag on each step to point that the steps should keep going even in case of error. {% end %} The first step is to walk all directories, trying to find out which ones contain the task configuration file. Once found, it will be added to the list of running tasks, and the steps will start running. One multiple tasks we could have something like this ([TUI](https://github.com/fdehau/tui-rs) idea, also): ``` Task Step 1 Step 2 Step 3 Step 4 ExampleTask Get list (R) Find file (W) Remove file (W) Task2 Step21 (D) Step 22 (R) Step 23 (W) Step 24 (W) Task3 Step31 (R) Step 32 (W) Step 33 (W) Task4 Step41 (D) Step 42 (R) Task5 Step51 (E) Step 52 (S) Step 53 (S) ``` In this example, there are 4 available cores, so only 4 steps can run at the same time; those are marked as `(R)` for Running. Once the step is Done, marked as `(D)`, task can move to the next step. Steps marked with `(W)` are Waiting for their time to be run, which requires the previous step to be marked as Done and having enough cores available. If a step errors, `(E)`, the task stops running and the next steps are marked as Skipped, `(S)`. The structures to load the tasks is pretty simple: ```rust pub struct Case { name: String, steps: Vec } pub struct Step { name: String, command: Command, } pub struct Command { command: String, name: String, } ``` After reading the data from the disk, the code could check if the step is "valid", like checking if the command exists and can be called. The `Command` doesn't exist in the configuration file, but it is generated while being loaded; the command itself it is the command specificed in the runner configuration file, but its main component is extracted and put in the name, to make it easier to display to the user; for example `/usr/bin/ls -lha` in the configuration file would have the same value in the `command` field, but the `name` would be simply "ls". For every structure, they is a "sister" structure "Run", with the results of the execution: ```rust pub struct CaseRun { case: &Case, status: CaseRunStatus, } pub enum CaseRunStatus { PendingSteps, // there are still steps to be run in this case Completed, // no more steps available Error, // one step failed and it shouldn't run anymore } pub struct StepRun { step: &Step, status: StepRunStatus, output: String, } pub enum StepRunStatus { Waiting, Running, Done, Error, Skipped, } ```