From 2250577cafc3015c3de29ee2974ab6afe4322700 Mon Sep 17 00:00:00 2001 From: Maxwell Jeffress Date: Thu, 29 Jan 2026 22:37:14 +1100 Subject: [PATCH] Initial commit --- README.md | 61 +++++++++++++++ main.cpp | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 README.md create mode 100644 main.cpp diff --git a/README.md b/README.md new file mode 100644 index 0000000..fff8c7c --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Spinny + +Spinny runs containers for you. + +## Installing + +Make sure to install `podman` first. + +Compile: + +``` +g++ main.cpp -o spinny +``` + +Install: + +``` +sudo cp spinny /usr/local/bin/spinny +``` + +Make a user to run Spinny under: + +``` +sudo useradd -m spinny +sudo passwd spinny +``` + +Start the SSH daemon (if you haven't) + +``` +sudo systemctl enable --now sshd +``` + +(Optional) Set up SSH keys to use with the `spinny` user. + +Build the containter used by Spinny: + +``` +ssh spinny@localhost +git clone https://chookspace.com/max/container +cd container +podman build -t dev-container . +podman run -it --rm dev-container +exit +``` + +Add /usr/local/bin/spinny to /etc/shells (use your editor) + +Change the `spinny` user's shell to `/usr/local/bin/spinny`: + +``` +sudo chsh -s /usr/local/bin/spinny spinny +``` + +Enjoy! + +``` +ssh spinny@localhost +``` + +Run the `help` command to see avaliable commands. diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..e29b180 --- /dev/null +++ b/main.cpp @@ -0,0 +1,216 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +std::map variables; +std::vector containers; + +std::vector lexCommand(const std::string& command) { + std::vector lexed; + std::string buf; + bool inString = false; + bool inVariable = false; + + for (const char& c : command) { + bool addToBuf = inString && inVariable; + switch (c) { + case '"': { + inString = !inString; + if (!inString) { + lexed.push_back(buf); + buf.clear(); + } + break; + } + case ' ': { + if (inString) { + buf += c; + } else if (inVariable) { + if (variables.find(buf) != variables.end()) { + if (!buf.empty()) lexed.push_back(variables[buf]); + buf.clear(); + } else { + lexed.push_back(""); + buf.clear(); + } + inVariable = false; + } else { + if (!buf.empty()) lexed.push_back(buf); + buf.clear(); + } + break; + } + case '$': { + if (addToBuf) { + buf += c; + } else { + inVariable = true; + } + break; + } + default: { + buf += c; + } + } + } + + if (!buf.empty()) lexed.push_back(buf); + + return lexed; +} + +void safeExecute(const std::vector& args) { + pid_t pid = fork(); + if (pid == 0) { + std::vector c_args; + for (const auto& arg : args) { + c_args.push_back(const_cast(arg.c_str())); + } + c_args.push_back(nullptr); + execvp(c_args[0], c_args.data()); + exit(1); + } else { + wait(NULL); + } +} + +void syncContainers() { + containers.clear(); + // Run podman command to get names of all containers + FILE* pipe = popen("podman ps -a --format '{{.Names}}'", "r"); + if (!pipe) return; + + char buffer[128]; + while (fgets(buffer, sizeof(buffer), pipe) != NULL) { + std::string name = buffer; + // Remove trailing newline + if (!name.empty() && name.back() == '\n') name.pop_back(); + containers.push_back(name); + } + pclose(pipe); +} + +int main(int argc, char** argv) { + std::cout << "Welcome to Spinny! Type a command to get started..." << std::endl; + + bool done = false; + std::string prompt; + while (!done) { + std::cout << "> "; + std::getline(std::cin, prompt); + auto command = lexCommand(prompt); + + if (!command.empty()) { + syncContainers(); + if (command[0] == "exit") { + done = true; + } + else if (command[0] == "set") { + if (command.size() < 3) { + std::cout << "Usage: set [key] [value]" << std::endl; + } else { + variables[command[1]] = command[2]; + } + } + else if (command[0] == "create") { + if (command.size() < 2) { + std::cout << "Usage: create [name]" << std::endl; + } else { + if (std::find(containers.begin(), containers.end(), command[1]) != containers.end()) { + std::cout << "Container " + command[1] + " already exists (use 'delete " + command[1] + "' to delete the existing container)" << std::endl; + } else { + safeExecute({"podman", "run", "-d", "--name", command[1], "dev-container", "tail", "-f", "/dev/null"}); + containers.push_back(command[1]); + std::cout << "Done! Enter your new container with 'enter " + command[1] + "'" << std::endl; + } + } + } + else if (command[0] == "start") { + if (command.size() < 2) { + std::cout << "Usage: start [name]" << std::endl; + } else { + if (std::find(containers.begin(), containers.end(), command[1]) != containers.end()) { + safeExecute({"podman", "start", command[1]}); + } else { + std::cout << "Container " + command[1] + " not found (use 'create " + command[1] + "' to make it)" << std::endl; + } + } + } + else if (command[0] == "delete") { + if (command.size() < 2) { + std::cout << "Usage: start [name]" << std::endl; + } else { + if (std::find(containers.begin(), containers.end(), command[1]) != containers.end()) { + std::string buf; + std::cout << "This will delete the container, and all files within it. Are you sure? (yes/no)"; + std::getline(std::cin, buf); + if (buf == "yes") { + containers.erase(std::remove(containers.begin(), containers.end(), command[1]), containers.end()); + safeExecute({"podman", "rm", command[1]}); + std::cout << "Deleted!" << std::endl; + } else { + std::cout << "*container breathes a sigh of relief*" << std::endl; + } + } else { + std::cout << "Container " + command[1] + " not found (use 'create " + command[1] + "' to make it)" << std::endl; + } + } + } + else if (command[0] == "stop") { + if (command.size() < 2) { + std::cout << "Usage: stop [name]" << std::endl; + } else { + if (std::find(containers.begin(), containers.end(), command[1]) != containers.end()) { + safeExecute({"podman", "stop", command[1]}); + } else { + std::cout << "Container " + command[1] + " not found (use 'create " + command[1] + "' to make it)" << std::endl; + } + } + } + else if (command[0] == "enter") { + if (command.size() < 2) { + std::cout << "Usage: enter [name]" << std::endl; + } else { + if (std::find(containers.begin(), containers.end(), command[1]) != containers.end()) { + safeExecute({"podman", "exec", "-it", command[1], "fish"}); + } else { + std::cout << "Container " + command[1] + " not found (use 'create " + command[1] + "' to make it)" << std::endl; + } + + } + } + else if (command[0] == "clear") { + safeExecute({"clear"}); + } + else if (command[0] == "help") { + std::cout << "Commands:" << std::endl; + std::cout << " create [name] - Creates an empty container" << std::endl; + std::cout << " enter [name] - Opens a shell in a container" << std::endl; + std::cout << " start [name] - Starts a container" << std::endl; + std::cout << " stop [name] - Stops a container" << std::endl; + std::cout << " delete [name] - Deletes a container" << std::endl; + std::cout << " tmp - Create a temporary container" << std::endl; + std::cout << " list - List containers" << std::endl; + std::cout << " exit - Exits Spinny" << std::endl; + } + else if (command[0] == "list" || command[0] == "ls") { + std::cout << "My containers:" << std::endl; + safeExecute({"podman", "ps", "-a"}); + } + else if (command[0] == "tmp") { + std::cout << "Spawning temporary container..." << std::endl; + safeExecute({"podman", "run", "-it", "--rm", "dev-container", "fish"}); + } else { + std::cout << "I don't know how to " + command[0] + " (run 'help' for commands)" << std::endl; + } + } + + } + std::cout << "Goodbye!" << std::endl; + return 0; +}