Skip to content

Commit

Permalink
Merge pull request #2 from SouthwestRoboticsProgramming/taskmanager
Browse files Browse the repository at this point in the history
Bring in TaskManager
  • Loading branch information
rmheuer authored Aug 31, 2023
2 parents 5e4f564 + 5f532be commit 81952e8
Show file tree
Hide file tree
Showing 17 changed files with 885 additions and 0 deletions.
37 changes: 37 additions & 0 deletions TaskManager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# TaskManager

TaskManager is a program that runs on coprocessors (i.e. Raspberry Pi, Jetson
Nano, etc). It manages the execution and deployment of other programs that run
on the coprocessors.

## Features

- Start tasks automatically when the robot turns on
- Restart tasks if they end unexpectedly
- Upload, edit, and delete task files over Messenger
- Send tasks' standard output and error over Messenger

## Configuration

The configuration is stored in `config.json` in the current working directory.
The JSON content is structured as follows:

```
Root object
├── messengerHost (string): Hostname of the Messenger server to use
├── messengerPort (integer): Port the Messenger server is running on
├── messengerName (string): Name to identify this Messenger client with the server
├── tasksRoot (string): Name of the folder to store task files in
└── maxFailCount (integer): Maximum number of failures after which a task is cancelled
```

Tasks can either be configured over Messenger using ShuffleLog, or manually
configured in `tasks.json`, which is structured as follows:

```
Root object
└── [Task Name] (object):
├── workingDirectory (string): Directory the task should run in, relative to the working directory of TaskManager
├── command (array of string): Command to execute the task. Each argument should be split into a separate string.
└── enabled (boolean): Whether the task is currently enabled. If it is not enabled, it will not be run.
```
36 changes: 36 additions & 0 deletions TaskManager/TaskManager-Core/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
plugins {
id 'java'
}

group 'com.swrobotics'
version '2023'

compileJava {
sourceCompatibility = '11'
targetCompatibility = '11'
}

dependencies {
implementation project(':Messenger:MessengerClient')
implementation project(':TaskManager:TaskManager-FileSystem')

implementation 'org.zeroturnaround:zt-exec:1.12'
implementation 'com.google.code.gson:gson:2.9.0'

// Disable SLF4J warnings
implementation 'org.slf4j:slf4j-nop:1.7.2'
}

jar {
dependsOn ':Messenger:MessengerClient:jar'
dependsOn ':TaskManager:TaskManager-FileSystem:jar'

manifest {
attributes 'Main-Class': 'com.swrobotics.taskmanager.TaskManagerMain'
}

from {
configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) }
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.swrobotics.taskmanager;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.File;
import java.io.IOException;

public final class FileTypeAdapter extends TypeAdapter<File> {
@Override
public void write(JsonWriter out, File value) throws IOException {
out.value(value.getPath());
}

@Override
public File read(JsonReader in) throws IOException {
return new File(in.nextString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.swrobotics.taskmanager;

public enum LogOutputType {
STDOUT,
STDERR
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.swrobotics.taskmanager;

import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.StartedProcess;

import java.io.File;
import java.io.IOException;

public final class Task {
// Settings
private File workingDirectory;
private String[] command;
private boolean enabled;

// Status
private final transient TaskManagerAPI api;
private final transient int maxFailCount;
private transient String name;
private transient boolean running;
private transient int failedStartCount;
private transient Process process;

public Task(
File workingDirectory,
String[] command,
boolean enabled,
TaskManagerAPI api,
int maxFailCount) {
this.api = api;
this.maxFailCount = maxFailCount;
this.workingDirectory = workingDirectory;
this.command = command;
this.enabled = enabled;

running = false;
failedStartCount = 0;
}

public Task(
File workingDirectory,
String[] command,
boolean enabled,
TaskManagerAPI api,
int maxFailCount,
String name) {
this(workingDirectory, command, enabled, api, maxFailCount);
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public void start() {
if (!enabled) return;

startProcess();
}

private void startProcess() {
try {
System.out.println("Starting task '" + name + "'");
StartedProcess p =
new ProcessExecutor()
.command(command)
.directory(workingDirectory)
.redirectOutput(new TaskOutputLogger(this, LogOutputType.STDOUT, api))
.redirectError(new TaskOutputLogger(this, LogOutputType.STDERR, api))
.start();
process = p.getProcess();
} catch (IOException e) {
System.err.println("Exception whilst starting task '" + name + "'");
e.printStackTrace();
}
}

public void restartIfProcessEnded() {
if (!enabled) return;
if (failedStartCount >= maxFailCount) return;
if (process != null && process.isAlive()) return;

if (process != null)
System.err.println(
"Process terminated unexpectedly for task '"
+ name
+ "' (exit code "
+ process.exitValue()
+ ")");
else System.err.println("Process not present for task '" + name + "'");

startProcess();
failedStartCount++;
if (failedStartCount == maxFailCount) {
System.err.println(
"Task '"
+ name
+ "' has exceeded maximum fail count of "
+ maxFailCount
+ ", it will not be restarted");
}
}

public void forceStop() {
if (!enabled || process == null || !process.isAlive()) return;

System.out.println("Stopping task '" + name + "'");

// Kill the process and its children
process.descendants()
.forEach(
(child) -> {
child.destroyForcibly();
});
process.destroyForcibly();
}

public File getWorkingDirectory() {
return workingDirectory;
}

public String[] getCommand() {
return command;
}

public boolean isEnabled() {
return enabled;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.swrobotics.taskmanager;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

public final class TaskManager {
private static final Type TASKS_MAP_TYPE = new TypeToken<Map<String, Task>>() {}.getType();

private static final File CONFIG_FILE = new File("config.json");
private static final File TASKS_FILE = new File("tasks.json");

private final Gson tasksGson;
private final TaskManagerAPI api;
private final Map<String, Task> tasks;

public TaskManager() {
TaskManagerConfiguration config = TaskManagerConfiguration.load(CONFIG_FILE);
api = new TaskManagerAPI(this, config);
tasksGson =
new GsonBuilder()
.registerTypeAdapter(File.class, new FileTypeAdapter())
.registerTypeAdapter(Task.class, new TaskSerializer(api, config))
.setPrettyPrinting()
.create();

tasks = loadTasks();

for (Map.Entry<String, Task> entry : tasks.entrySet()) {
Task task = entry.getValue();
task.setName(entry.getKey());
task.start();
}

saveTasks();
}

private Map<String, Task> loadTasks() {
// If the file doesn't exist, there must not be any tasks yet
if (!TASKS_FILE.exists()) return new HashMap<>();

try {
return tasksGson.fromJson(new FileReader(TASKS_FILE), TASKS_MAP_TYPE);
} catch (Exception e) {
throw new RuntimeException("Failed to load tasks file", e);
}
}

private void saveTasks() {
try {
FileWriter writer = new FileWriter(TASKS_FILE);
tasksGson.toJson(tasks, writer);
writer.close();
} catch (Exception e) {
System.err.println("Failed to save tasks file");
e.printStackTrace();
}
}

public void addTask(Task task) {
tasks.put(task.getName(), task);
saveTasks();
}

public Task getTask(String name) {
return tasks.get(name);
}

public void removeTask(String name) {
Task removed = tasks.remove(name);
if (removed != null) {
removed.forceStop();
saveTasks();
}
}

public Map<String, Task> getTasks() {
return new HashMap<>(tasks);
}

public void run() {
while (true) {
api.read();
for (Task task : tasks.values()) {
task.restartIfProcessEnded();
}

try {
Thread.sleep(1000 / 50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Loading

0 comments on commit 81952e8

Please sign in to comment.