From 5f532be1b5c7336bb68db5aaa9c18840d941111b Mon Sep 17 00:00:00 2001 From: rmheuer <63077980+rmheuer@users.noreply.github.com> Date: Wed, 30 Aug 2023 19:03:49 -0500 Subject: [PATCH] Bring in TaskManager --- TaskManager/README.md | 37 +++ TaskManager/TaskManager-Core/build.gradle | 36 +++ .../taskmanager/FileTypeAdapter.java | 20 ++ .../swrobotics/taskmanager/LogOutputType.java | 6 + .../java/com/swrobotics/taskmanager/Task.java | 132 +++++++++++ .../swrobotics/taskmanager/TaskManager.java | 102 ++++++++ .../taskmanager/TaskManagerAPI.java | 134 +++++++++++ .../taskmanager/TaskManagerConfiguration.java | 65 +++++ .../taskmanager/TaskManagerMain.java | 8 + .../taskmanager/TaskOutputLogger.java | 25 ++ .../taskmanager/TaskSerializer.java | 41 ++++ .../TaskManager-FileSystem/build.gradle | 19 ++ .../taskmanager/filesystem/FileSystemAPI.java | 223 ++++++++++++++++++ TaskManager/config.json | 7 + TaskManager/tasks.json | 18 ++ TaskManager/tasks/test/test.sh | 10 + settings.gradle | 2 + 17 files changed, 885 insertions(+) create mode 100644 TaskManager/README.md create mode 100644 TaskManager/TaskManager-Core/build.gradle create mode 100644 TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/FileTypeAdapter.java create mode 100644 TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/LogOutputType.java create mode 100644 TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/Task.java create mode 100644 TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManager.java create mode 100644 TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerAPI.java create mode 100644 TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerConfiguration.java create mode 100644 TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerMain.java create mode 100644 TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskOutputLogger.java create mode 100644 TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskSerializer.java create mode 100644 TaskManager/TaskManager-FileSystem/build.gradle create mode 100644 TaskManager/TaskManager-FileSystem/src/main/java/com/swrobotics/taskmanager/filesystem/FileSystemAPI.java create mode 100644 TaskManager/config.json create mode 100644 TaskManager/tasks.json create mode 100644 TaskManager/tasks/test/test.sh diff --git a/TaskManager/README.md b/TaskManager/README.md new file mode 100644 index 0000000..20eced0 --- /dev/null +++ b/TaskManager/README.md @@ -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. +``` diff --git a/TaskManager/TaskManager-Core/build.gradle b/TaskManager/TaskManager-Core/build.gradle new file mode 100644 index 0000000..b18d083 --- /dev/null +++ b/TaskManager/TaskManager-Core/build.gradle @@ -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) } + } +} diff --git a/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/FileTypeAdapter.java b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/FileTypeAdapter.java new file mode 100644 index 0000000..11e337a --- /dev/null +++ b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/FileTypeAdapter.java @@ -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 { + @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()); + } +} diff --git a/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/LogOutputType.java b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/LogOutputType.java new file mode 100644 index 0000000..bedc00b --- /dev/null +++ b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/LogOutputType.java @@ -0,0 +1,6 @@ +package com.swrobotics.taskmanager; + +public enum LogOutputType { + STDOUT, + STDERR +} diff --git a/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/Task.java b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/Task.java new file mode 100644 index 0000000..b6d7773 --- /dev/null +++ b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/Task.java @@ -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; + } +} diff --git a/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManager.java b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManager.java new file mode 100644 index 0000000..a47625b --- /dev/null +++ b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManager.java @@ -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>() {}.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 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 entry : tasks.entrySet()) { + Task task = entry.getValue(); + task.setName(entry.getKey()); + task.start(); + } + + saveTasks(); + } + + private Map 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 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(); + } + } + } +} diff --git a/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerAPI.java b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerAPI.java new file mode 100644 index 0000000..86558d5 --- /dev/null +++ b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerAPI.java @@ -0,0 +1,134 @@ +package com.swrobotics.taskmanager; + +import com.swrobotics.messenger.client.MessageBuilder; +import com.swrobotics.messenger.client.MessageReader; +import com.swrobotics.messenger.client.MessengerClient; +import com.swrobotics.taskmanager.filesystem.FileSystemAPI; + +import java.io.File; +import java.util.Map; + +public final class TaskManagerAPI { + // Tasks API + private static final String MSG_LIST_TASKS = ":ListTasks"; + private static final String MSG_CREATE_TASK = ":CreateTask"; + private static final String MSG_DELETE_TASK = ":DeleteTask"; + private static final String MSG_TASKS = ":Tasks"; + + // Logging + private static final String MSG_STDOUT = ":StdOut:"; + private static final String MSG_STDERR = ":StdErr:"; + + private final TaskManager mgr; + private final TaskManagerConfiguration config; + private final MessengerClient msg; + + private final String msgTasks; + private final String msgStdOut; + private final String msgStdErr; + + private final File tasksRoot; + + public TaskManagerAPI(TaskManager mgr, TaskManagerConfiguration config) { + this.mgr = mgr; + this.config = config; + + System.out.println( + "Connecting to Messenger at " + + config.getMessengerHost() + + ":" + + config.getMessengerPort() + + " as " + + config.getMessengerName()); + msg = + new MessengerClient( + config.getMessengerHost(), + config.getMessengerPort(), + config.getMessengerName()); + + String prefix = config.getMessengerName(); + new FileSystemAPI(msg, prefix, config.getTasksRoot()); + + String msgListTasks = prefix + MSG_LIST_TASKS; + String msgCreateTask = prefix + MSG_CREATE_TASK; + String msgDeleteTask = prefix + MSG_DELETE_TASK; + + msgTasks = prefix + MSG_TASKS; + msgStdOut = prefix + MSG_STDOUT; + msgStdErr = prefix + MSG_STDERR; + + tasksRoot = config.getTasksRoot(); + if (!tasksRoot.exists()) tasksRoot.mkdirs(); + + msg.addHandler(msgListTasks, this::onListTasks); + msg.addHandler(msgCreateTask, this::onCreateTask); + msg.addHandler(msgDeleteTask, this::onDeleteTask); + } + + private String removeTrailingSeparator(String path) { + if (path.endsWith(File.separator)) return path.substring(0, path.length() - 1); + return path; + } + + private String getTaskPath(File file) { + String rootAbsolute = removeTrailingSeparator(tasksRoot.getAbsolutePath()); + String fileAbsolute = removeTrailingSeparator(file.getAbsolutePath()); + + if (!fileAbsolute.startsWith(rootAbsolute)) + throw new AssertionError("File is not a task tile: " + file); + + return fileAbsolute.substring(rootAbsolute.length()); + } + + private void onListTasks(String type, MessageReader reader) { + MessageBuilder out = msg.prepare(msgTasks); + Map tasks = mgr.getTasks(); + + out.addInt(tasks.size()); + for (Map.Entry entry : tasks.entrySet()) { + out.addString(entry.getKey()); + + Task task = entry.getValue(); + out.addString(getTaskPath(task.getWorkingDirectory())); + String[] command = task.getCommand(); + out.addInt(command.length); + for (String token : command) out.addString(token); + out.addBoolean(task.isEnabled()); + } + + out.send(); + } + + // Can also be used to modify a task by overwriting an existing one + private void onCreateTask(String type, MessageReader reader) { + String name = reader.readString(); + String workingDirPath = reader.readString(); + File workingDir = new File(tasksRoot, workingDirPath); + int commandSize = reader.readInt(); + String[] command = new String[commandSize]; + for (int i = 0; i < commandSize; i++) { + command[i] = reader.readString(); + } + boolean enabled = reader.readBoolean(); + Task task = new Task(workingDir, command, enabled, this, config.getMaxFailCount(), name); + + // Remove old task + if (mgr.getTask(name) != null) mgr.removeTask(name); + + mgr.addTask(task); + } + + private void onDeleteTask(String type, MessageReader reader) { + mgr.removeTask(reader.readString()); + } + + public void broadcastTaskOutput(Task task, LogOutputType type, String line) { + msg.prepare((type == LogOutputType.STDOUT ? msgStdOut : msgStdErr) + task.getName()) + .addString(line) + .send(); + } + + public void read() { + msg.readMessages(); + } +} diff --git a/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerConfiguration.java b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerConfiguration.java new file mode 100644 index 0000000..07c19aa --- /dev/null +++ b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerConfiguration.java @@ -0,0 +1,65 @@ +package com.swrobotics.taskmanager; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; + +public final class TaskManagerConfiguration { + public static Gson GSON = + new GsonBuilder() + .registerTypeAdapter(File.class, new FileTypeAdapter()) + .setPrettyPrinting() + .create(); + + public static TaskManagerConfiguration load(File file) { + try { + return GSON.fromJson(new FileReader(file), TaskManagerConfiguration.class); + } catch (FileNotFoundException e) { + TaskManagerConfiguration conf = new TaskManagerConfiguration(); + + System.err.println("Config file not found, saving default"); + try { + FileWriter writer = new FileWriter(file); + GSON.toJson(conf, writer); + writer.close(); + } catch (Exception e2) { + System.err.println("Failed to save default config file"); + e2.printStackTrace(); + } + + return conf; + } + } + + private String messengerHost = "localhost"; + private int messengerPort = 5805; + private String messengerName = "TaskManager"; + private File tasksRoot = new File("tasks"); + private int maxFailCount = 10; + + private TaskManagerConfiguration() {} + + public String getMessengerHost() { + return messengerHost; + } + + public int getMessengerPort() { + return messengerPort; + } + + public String getMessengerName() { + return messengerName; + } + + public File getTasksRoot() { + return tasksRoot; + } + + public int getMaxFailCount() { + return maxFailCount; + } +} diff --git a/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerMain.java b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerMain.java new file mode 100644 index 0000000..91fd4ea --- /dev/null +++ b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskManagerMain.java @@ -0,0 +1,8 @@ +package com.swrobotics.taskmanager; + +public final class TaskManagerMain { + public static void main(String[] args) { + TaskManager manager = new TaskManager(); + manager.run(); + } +} diff --git a/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskOutputLogger.java b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskOutputLogger.java new file mode 100644 index 0000000..3dfb9eb --- /dev/null +++ b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskOutputLogger.java @@ -0,0 +1,25 @@ +package com.swrobotics.taskmanager; + +import org.zeroturnaround.exec.stream.LogOutputStream; + +public final class TaskOutputLogger extends LogOutputStream { + private final Task task; + private final LogOutputType type; + private final TaskManagerAPI api; + + public TaskOutputLogger(Task task, LogOutputType type, TaskManagerAPI api) { + this.task = task; + this.type = type; + this.api = api; + } + + @Override + protected void processLine(String line) { + api.broadcastTaskOutput(task, type, line); + if (type == LogOutputType.STDOUT) { + System.out.println("[" + task.getName() + "/Out] " + line); + } else { + System.err.println("[" + task.getName() + "/Err] " + line); + } + } +} diff --git a/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskSerializer.java b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskSerializer.java new file mode 100644 index 0000000..89a0aec --- /dev/null +++ b/TaskManager/TaskManager-Core/src/main/java/com/swrobotics/taskmanager/TaskSerializer.java @@ -0,0 +1,41 @@ +package com.swrobotics.taskmanager; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.io.File; +import java.lang.reflect.Type; + +public final class TaskSerializer implements JsonSerializer, JsonDeserializer { + private final TaskManagerAPI api; + private final TaskManagerConfiguration config; + + public TaskSerializer(TaskManagerAPI api, TaskManagerConfiguration config) { + this.api = api; + this.config = config; + } + + @Override + public Task deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + JsonObject obj = json.getAsJsonObject(); + File workingDir = context.deserialize(obj.get("workingDirectory"), File.class); + String[] command = context.deserialize(obj.get("command"), String[].class); + boolean enabled = obj.get("enabled").getAsBoolean(); + return new Task(workingDir, command, enabled, api, config.getMaxFailCount()); + } + + @Override + public JsonElement serialize(Task src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.add("workingDirectory", context.serialize(src.getWorkingDirectory())); + obj.add("command", context.serialize(src.getCommand())); + obj.addProperty("enabled", src.isEnabled()); + return obj; + } +} diff --git a/TaskManager/TaskManager-FileSystem/build.gradle b/TaskManager/TaskManager-FileSystem/build.gradle new file mode 100644 index 0000000..cdb8d0a --- /dev/null +++ b/TaskManager/TaskManager-FileSystem/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java-library' +} + +group 'com.swrobotics' +version '2023' + +compileJava { + sourceCompatibility = '11' + targetCompatibility = '11' +} + +dependencies { + api project(':Messenger:MessengerClient') +} + +jar { + dependsOn ':Messenger:MessengerClient:jar' +} diff --git a/TaskManager/TaskManager-FileSystem/src/main/java/com/swrobotics/taskmanager/filesystem/FileSystemAPI.java b/TaskManager/TaskManager-FileSystem/src/main/java/com/swrobotics/taskmanager/filesystem/FileSystemAPI.java new file mode 100644 index 0000000..69ada26 --- /dev/null +++ b/TaskManager/TaskManager-FileSystem/src/main/java/com/swrobotics/taskmanager/filesystem/FileSystemAPI.java @@ -0,0 +1,223 @@ +package com.swrobotics.taskmanager.filesystem; + +import com.swrobotics.messenger.client.MessageBuilder; +import com.swrobotics.messenger.client.MessageReader; +import com.swrobotics.messenger.client.MessengerClient; + +import java.io.*; +import java.nio.file.Files; + +public final class FileSystemAPI { + private static final String MSG_LIST_FILES = ":ListFiles"; + private static final String MSG_READ_FILE = ":ReadFile"; + private static final String MSG_WRITE_FILE = ":WriteFile"; + private static final String MSG_DELETE_FILE = ":DeleteFile"; + private static final String MSG_MOVE_FILE = ":MoveFile"; + private static final String MSG_MKDIR = ":Mkdir"; + private static final String MSG_FILES = ":Files"; + private static final String MSG_FILE_CONTENT = ":FileContent"; + private static final String MSG_WRITE_CONFIRM = ":WriteConfirm"; + private static final String MSG_DELETE_CONFIRM = ":DeleteConfirm"; + private static final String MSG_MOVE_CONFIRM = ":MoveConfirm"; + private static final String MSG_MKDIR_CONFIRM = ":MkdirConfirm"; + + private final MessengerClient msg; + private final File rootDir; + + private final String msgFiles; + private final String msgFileContent; + private final String msgWriteConfirm; + private final String msgDeleteConfirm; + private final String msgMoveConfirm; + private final String msgMkdirConfirm; + + public FileSystemAPI(MessengerClient msg, String prefix, File rootDir) { + this.msg = msg; + this.rootDir = rootDir; + + String msgListFiles = prefix + MSG_LIST_FILES; + String msgReadFile = prefix + MSG_READ_FILE; + String msgWriteFile = prefix + MSG_WRITE_FILE; + String msgDeleteFile = prefix + MSG_DELETE_FILE; + String msgMoveFile = prefix + MSG_MOVE_FILE; + String msgMkdir = prefix + MSG_MKDIR; + + msgFiles = prefix + MSG_FILES; + msgFileContent = prefix + MSG_FILE_CONTENT; + msgWriteConfirm = prefix + MSG_WRITE_CONFIRM; + msgDeleteConfirm = prefix + MSG_DELETE_CONFIRM; + msgMoveConfirm = prefix + MSG_MOVE_CONFIRM; + msgMkdirConfirm = prefix + MSG_MKDIR_CONFIRM; + + msg.addHandler(msgListFiles, this::onListFiles); + msg.addHandler(msgReadFile, this::onReadFile); + msg.addHandler(msgWriteFile, this::onWriteFile); + msg.addHandler(msgMoveFile, this::onMoveFile); + msg.addHandler(msgDeleteFile, this::onDeleteFile); + msg.addHandler(msgMkdir, this::onMkdir); + } + + private String localizePath(String path) { + return path.replace('/', File.separatorChar); + } + + private byte[] readFile(File file) throws IOException { + FileInputStream in = new FileInputStream(file); + ByteArrayOutputStream b = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) > 0) { + b.write(buffer, 0, read); + } + in.close(); + b.close(); + return b.toByteArray(); + } + + private boolean deleteFile(File file) { + File[] contents = file.listFiles(); + if (contents != null) { + for (File f : contents) { + if (!Files.isSymbolicLink(f.toPath())) { + if (!deleteFile(f)) return false; + } + } + } + return file.delete(); + } + + private boolean moveFile(File src, File dst) { + return src.renameTo(dst); + } + + private void onListFiles(String type, MessageReader reader) { + MessageBuilder out = msg.prepare(msgFiles); + + String dirPath = reader.readString(); + File dir = new File(rootDir, localizePath(dirPath)); + out.addString(dirPath); + if (!dir.exists() || !dir.isDirectory()) { + out.addBoolean(false); + out.send(); + return; + } + + File[] children = dir.listFiles(); + out.addBoolean(true); + if (children == null) { + out.addInt(0); + } else { + out.addInt(children.length); + for (File child : children) { + out.addString(child.getName()); + out.addBoolean(child.isDirectory()); + } + } + + out.send(); + } + + private void onReadFile(String type, MessageReader reader) { + MessageBuilder out = msg.prepare(msgFileContent); + + String path = reader.readString(); + File file = new File(rootDir, localizePath(path)); + out.addString(path); + if (!file.exists() || !file.isFile()) { + out.addBoolean(false); + out.send(); + return; + } + + System.out.println("Sending contents of " + path); + try { + byte[] fileContent = readFile(file); + out.addBoolean(true); + out.addInt(fileContent.length); + out.addRaw(fileContent); + } catch (IOException e) { + out.addBoolean(false); + System.err.println("Reading file content failed for " + path); + e.printStackTrace(); + } + + out.send(); + } + + private void onWriteFile(String type, MessageReader reader) { + String path = reader.readString(); + File file = new File(rootDir, localizePath(path)); + if (file.exists() && !file.isFile()) { + msg.prepare(msgWriteConfirm).addString(path).addBoolean(false).send(); + return; + } + + int dataLen = reader.readInt(); + byte[] data = reader.readRaw(dataLen); + + try (FileOutputStream fos = new FileOutputStream(file)) { + System.out.println("Receiving file data for " + path); + fos.write(data); + fos.flush(); + + msg.prepare(msgWriteConfirm).addString(path).addBoolean(true).send(); + } catch (IOException e) { + System.err.println("File write failed for " + path); + e.printStackTrace(); + + msg.prepare(msgWriteConfirm).addString(path).addBoolean(false).send(); + } + } + + private void onDeleteFile(String type, MessageReader reader) { + String path = reader.readString(); + File file = new File(rootDir, localizePath(path)); + if (!file.exists()) { + msg.prepare(msgDeleteConfirm).addString(path).addBoolean(false).send(); + return; + } + + System.out.println("Deleting " + path); + boolean result = deleteFile(file); + if (!result) System.err.println("File delete failed for " + path); + + msg.prepare(msgDeleteConfirm).addString(path).addBoolean(result).send(); + } + + private void onMoveFile(String type, MessageReader reader) { + String srcPath = reader.readString(); + String dstPath = reader.readString(); + File srcFile = new File(rootDir, localizePath(srcPath)); + File dstFile = new File(rootDir, localizePath(dstPath)); + + if (!srcFile.exists() || dstFile.exists()) { + msg.prepare(msgMoveConfirm) + .addString(srcPath) + .addString(dstPath) + .addBoolean(false) + .send(); + return; + } + + System.out.println("Moving " + srcPath + " to " + dstPath); + boolean result = moveFile(srcFile, dstFile); + if (!result) System.err.println("Failed to move " + srcPath + " to " + dstPath); + + msg.prepare(msgMoveConfirm).addString(srcPath).addString(dstPath).addBoolean(result).send(); + } + + private void onMkdir(String type, MessageReader reader) { + String path = reader.readString(); + File file = new File(rootDir, localizePath(path)); + if (file.exists() && !file.isDirectory()) { + msg.prepare(msgMkdirConfirm).addString(path).addBoolean(false).send(); + return; + } + + System.out.println("Creating directory " + path); + boolean result = file.mkdirs(); + if (!result) System.err.println("Mkdir failed for " + path); + + msg.prepare(msgMkdirConfirm).addString(path).addBoolean(result).send(); + } +} diff --git a/TaskManager/config.json b/TaskManager/config.json new file mode 100644 index 0000000..b355acf --- /dev/null +++ b/TaskManager/config.json @@ -0,0 +1,7 @@ +{ + "messengerHost": "localhost", + "messengerPort": 5805, + "messengerName": "TaskManager", + "tasksRoot": "tasks", + "maxFailCount": 10 +} diff --git a/TaskManager/tasks.json b/TaskManager/tasks.json new file mode 100644 index 0000000..cb03460 --- /dev/null +++ b/TaskManager/tasks.json @@ -0,0 +1,18 @@ +{ + "Test Task 2": { + "workingDirectory": "tasks/test", + "command": [ + "bash", + "test.sh" + ], + "enabled": false + }, + "The thing": { + "workingDirectory": "tasks/somthing", + "command": [ + "bash", + "test.sh" + ], + "enabled": false + } +} diff --git a/TaskManager/tasks/test/test.sh b/TaskManager/tasks/test/test.sh new file mode 100644 index 0000000..b3f5bf3 --- /dev/null +++ b/TaskManager/tasks/test/test.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +echo "Starting the task" +i=0 +while : +do + echo "Running the task: $i" + sleep 1 + i=$((i+1)) +done diff --git a/settings.gradle b/settings.gradle index ce5c547..35b8f22 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,5 @@ rootProject.name = "2024_Main_Robot" include 'Messenger:MessengerClient' +include 'TaskManager:TaskManager-Core' +include 'TaskManager:TaskManager-FileSystem'