-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from SouthwestRoboticsProgramming/taskmanager
Bring in TaskManager
- Loading branch information
Showing
17 changed files
with
885 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/FileTypeAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/LogOutputType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.swrobotics.taskmanager; | ||
|
||
public enum LogOutputType { | ||
STDOUT, | ||
STDERR | ||
} |
132 changes: 132 additions & 0 deletions
132
TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/Task.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.