CustomStartupRunner/main.cpp

653 lines
24 KiB
C++
Raw Normal View History

2025-09-20 20:20:47 +05:30
#include <filesystem>
#include <fstream>
#include <chrono>
#include <format>
2026-05-14 15:27:08 +05:30
#include <nlohmann/json.hpp>
2025-09-20 20:20:47 +05:30
#include <iostream>
#include <sstream>
2026-05-14 15:27:08 +05:30
#include <deque>
#include <vector>
#include <cctype>
#include "appinfo.h"
#include "executionmanager.h"
#include "log.h"
extern "C" {
#include <sys/types.h>
#include <pwd.h>
2025-09-20 20:20:47 +05:30
#include <unistd.h>
#include <cstdlib>
2026-05-14 15:27:08 +05:30
}
2025-09-20 20:20:47 +05:30
char *get_current_time() {
2026-05-14 15:27:08 +05:30
const auto now = std::chrono::system_clock::now();
const std::time_t t_c = std::chrono::system_clock::to_time_t(now);
return std::ctime(&t_c);
}
struct Options {
std::string json_file_path = "";
std::string log_directory = "";
long long int log_file_max_size = 10 * 1024 * 1024;
int threads = 16;
bool check_for_errors = false;
public:
Options() {
const char *home = std::getenv("HOME");
if(!home || strlen(home) <= 0)
{
log_directory = "StartUpApps/logs";
return;
}
using fp = std::filesystem::path;
fp fs = fp(home) / ".local" / "share" / "StartUpApps" / "logs";
log_directory = fs.string();
}
2026-05-14 15:27:08 +05:30
}options;
void createConfig() {
using json = nlohmann::json;
json config;
config["log_file_max_size"] = 10 * 1024 * 1024;
std::string str = config.dump();
std::fstream file_out("config.json", std::fstream::out);
file_out << str;
file_out.flush();
file_out.close();
}
void parseConfig() {
using json = nlohmann::json;
if(!std::filesystem::exists("config.json")) {
std::cout << "No config file creating one." << std::endl;
createConfig();
}
std::fstream file_in("config.json", std::fstream::in);
std::stringstream ss;
char data;
while(!file_in.eof()) {
file_in.get(data);
if(file_in.eof())
break;
ss << data;
}
json config = json::parse(ss.str());
if(config.find("log_file_max_size") != config.end() && config["log_file_max_size"].is_number_integer()) {
options.log_file_max_size = config["log_file_max_size"].get<long long int>();
}
}
std::string strip(std::string& t) {
std::string g;
for(auto i: t) {
if(i == ' ' || i == '\t' || i == '\n' || i == '\v' || i == '\r')
continue;
else
g += i;
}
return g;
}
void printHelp() {
std::cout << R"(Usage: StartUpRunner [OPTIONS]
OPTIONS:
--help|-h : Prints this screen.
--version|-v : Prints version info.
--threads|-t [T] : Sets number of threads to [T].
--log_dir|-ld [Dir] : Sets log directory to [Dir].
--defaults|-d : Prints the default values used if no arguments are provided.
--json|-j [FileName]: Set Json Startup List to FileName.
--test|-T [FileName]: Check the json file for errors.
For a detailed info about the json startup file use --help json.)" << std::endl;
2026-05-14 15:27:08 +05:30
}
void printVersion() {
std::cout << "StartUpRunner: Version-0.0.3 Pre-Alpha Pre-BigBang Era Edition." << std::endl;
2026-05-14 15:27:08 +05:30
}
void printDefaults() {
std::cout << "StartUpRunner Default Values: " << std::endl;
std::cout << "Json file : " << options.json_file_path << std::endl;
std::cout << "Logs directory : " << options.log_directory << std::endl;
std::cout << "Log file max size: " << options.log_file_max_size << std::endl;
2026-05-14 15:27:08 +05:30
std::cout << "Threads : " << options.threads << std::endl;
}
void printJsonHelp() {
std::cout << R"(Json StartUp List file format:
Top level is an Array of Apps.
Each App Entry contains:
[REQUIRED] name[string] : Name of the module.
[REQUIRED] app_name[string] : Path to the program with its name.
[REQUIRED] run_mode[string] : Mode can be daemon or script.
[OPTIONAL] app_args[list[string]] : List of arguments to the program.
Mode daemon : The app forks the program and exits without waiting.
Mode script : The app forks the program and waits for it to finish execution.
If no run_mode is given it is presumed to be script.
[OPTIONAL] after[{name,state}] : List containing names and state of modules.
name[string] : Name of the other module.
state[string]: State of the other module.
State exited : Specified module must have finished execution.
State running : Specified module must be running.
This module will be launched after the module foundin this list using name is at the state specified as state.
[OPTIONAL] env{key: value} : Additional enviroment variables.
Every key in this object will be used as environment name,
and every corresponding value as its value.
[OPTIONAL] working_dir[string] : Working directory of the program.
[OPTIONAL] create_log[boolean] : Create a log file at logs path.
[OPTIONAL] run_as[string] : Run as a particular user.
[OPTIONAL] run_every[string] : Run task at every specified interval.
format should be like:
[digits][unit]
valid units are:
s or S: seconds
m: minutes
h or H: hours
d or D: days
w or W: weeks
M: months
Example: 15s
Example: 2h
[OPTIONAL] run_at[string] : Run task at every specified time.
Format of string should be like:
"[time] Daily": for running daily at that time
"[day] [time] Weekly": for running every week on those days at that time.
"[day_month] [time] Monthly": for running every month on those days at that time.
[time] format is 24Hrs with [HH:MM:SS]
[day] format is any of [Mon, Tue, Wed, Thu, Fri, Sat, Sun]
[day_month] format is 1-31.
For Weekly, [day] can be a list of days seperated by commas.
For Monthly, [day_month] can be a list of days seperated by commas.
For Any, [time] can be a list of timestamps seperated by commas.
[time] Daily: for running daily at that time.
[day] [time] Weekly: for running every week on those days at that time.
[OPTIONAL] run_on_start[boolean] : Run on initial startup as well [Only valid for scheduled tasks].
Example startup_apps.json:
2026-05-14 15:27:08 +05:30
[
{
"name": "LS 0",
"app_name": "/bin/ls",
"app_args": ["-l"],
"run_mode": "script",
"create_log": true,
"working_dir": "/home/"
},
{
"name": "LS 1",
"app_name": "/bin/ls",
"app_args": ["-l"],
"run_mode": "script",
"create_log": true,
"after": [{"module": "LS 0", "state": "exited"}],
"env": {"PATH": "/home/:/usr/bin/", "NAME": "ONE"},
"working_dir": "/home/etc",
"run_as": "username",
"run_every": "10s",
"run_on_start": true
},
{
"name": "LS 2",
"app_name": "/bin/ls",
"app_args": ["-lh"],
"run_mode": "script",
"create_log": true,
"after": [{"module": "LS 0", "state": "exited"}],
"working_dir": "/home/etc/Documents",
"run_on_start": false,
"run_at": "1,15,30 01:30:00 Monthly"
2026-05-14 15:27:08 +05:30
}
]
)";
2026-05-14 15:27:08 +05:30
}
int contains(std::vector<std::string> &list, std::string l, std::string v) {
for(int i = 0; i < list.size(); i++) {
if(list[i] == l || list[i] == v)
return i;
}
return -1;
}
int toint(std::string s) {
std::stringstream ss;
int ret;
ss << s;
ss >> ret;
return ret;
}
void parseArgs(int c, char *v[]) {
if(c == 1)
return;
if(c == 2) {
std::string arg1(v[1]);
if(arg1 == "--help" || arg1 == "-h") {
printHelp();
exit(0);
}
if(arg1 == "--version" || arg1 == "-v") {
printVersion();
exit(0);
}
if(arg1 == "--defaults" || arg1 == "-d") {
printDefaults();
exit(0);
}
2026-05-14 15:27:08 +05:30
}
if(c >= 2) {
2026-05-14 15:27:08 +05:30
std::vector<std::string> arg_list;
for(int i=1; i<c; i++) {
arg_list.push_back(v[i]);
}
bool contains_any_known_arg = false;
int i = contains(arg_list, "--json", "-j");
if(i >= 0) {
int j = i + 1;
if(j >= arg_list.size()) {
2026-05-14 15:27:08 +05:30
printHelp();
exit(-1);
}
options.json_file_path = arg_list[j];
std::cout << "Json file path: " << options.json_file_path << std::endl;
contains_any_known_arg = true;
}
i = contains(arg_list, "--log_dir", "-ld");
if(i >= 0) {
int j = i + 1;
if(j >= arg_list.size()) {
2026-05-14 15:27:08 +05:30
printHelp();
exit(-1);
}
options.log_directory = arg_list[j];
std::cout << "Log directory: " << options.log_directory << std::endl;
contains_any_known_arg = true;
}
i = contains(arg_list, "--threads", "-t");
if(i >= 0) {
int j = i + 1;
if(j >= arg_list.size()) {
2026-05-14 15:27:08 +05:30
printHelp();
exit(-1);
}
options.threads = toint(arg_list[j]);
std::cout << "Threads: " << options.threads << std::endl;
contains_any_known_arg = true;
}
i = contains(arg_list, "--test", "-T");
if(i >= 0) {
options.check_for_errors = true;
int j = i + 1;
if(j < arg_list.size() && arg_list[j].rfind("-", 0) != 0) {
options.json_file_path = arg_list[j];
}
std::cout << "Testing json file." << std::endl;
contains_any_known_arg = true;
}
2026-05-14 15:27:08 +05:30
i = contains(arg_list, "--help", "-h");
if(i >= 0) {
int j = i + 1;
if(j < arg_list.size() && arg_list[j] == "json") {
2026-05-14 15:27:08 +05:30
printJsonHelp();
exit(0);
}
else {
printHelp();
exit(0);
2026-05-14 15:27:08 +05:30
}
contains_any_known_arg = true;
}
if(!contains_any_known_arg) {
printHelp();
exit(-1);
}
}
}
std::string string_strip(std::string str) {
std::string ch;
bool front_found = false;
for(int i=0; i<str.size(); i++) {
if(!front_found) {
2026-05-14 17:06:59 +05:30
if(str[i] == ' ' || str[i] == '\t' || str[i] == '\n' || str[i] == '\v' || str[i] == '\r')
2026-05-14 15:27:08 +05:30
continue;
else {
front_found = true;
ch += str[i];
continue;
}
}
else
ch += str[i];
}
2026-05-14 17:06:59 +05:30
auto pos = ch.find_last_not_of(" \t\n\v\r");
2026-05-14 15:27:08 +05:30
if(pos == std::string::npos)
return ch;
ch.erase(pos+1);
return ch;
}
std::vector<std::string> string_split(std::string s, const std::string& delimiter) {
std::vector<std::string> tokens;
size_t pos = 0;
std::string token;
while ((pos = s.find(delimiter)) != std::string::npos) {
token = string_strip(s.substr(0, pos));
if(!token.empty())
tokens.push_back(token);
2026-05-14 15:27:08 +05:30
s.erase(0, pos + delimiter.length());
}
std::string last = string_strip(s);
if(!last.empty())
tokens.push_back(last);
2026-05-14 15:27:08 +05:30
return tokens;
}
std::string tolower(std::string str) {
std::string s;
std::transform(str.begin(), str.end(), std::back_inserter(s), [](char c){
return std::tolower(c);
});
return s;
}
void printListModules(const std::vector<std::shared_ptr<AppInfo>>& item_list, std::ostream &out) {
std::stringstream ss;
int s = item_list.size();
ss << "List Contains --> ";
for(auto &i: item_list) {
s--;
if(s > 0)
ss << i->getModuleName() << ", ";
else
ss << i->getModuleName();
}
out << ss.str() << std::endl;
}
bool listContainsAfter(const std::vector<std::shared_ptr<AppInfo>> &list, const AppInfo::AppData &item) {
std::map<std::string, bool> contains_list;
std::string it = item.getAfterKeys();
auto string_list = string_split(it, ",");
for(auto &i: string_list) {
bool found = false;
for(auto &j: list) {
if(tolower(string_strip(j->getModuleName())) == tolower(string_strip(i))) {
contains_list[i] = true;
found = true;
break;
}
}
if(!found)
contains_list[i] = false;
}
bool value = std::all_of(contains_list.begin(), contains_list.end(),
[](const auto &it) {
return it.second;
});
return value;
}
std::optional<int> checkAppList(std::deque<AppInfo::AppData> &copy_list, std::vector<std::shared_ptr<AppInfo>> &after_list, std::ostream &out) {
out << "Checking app list with: " << copy_list.size() << " apps for errors..." << std::endl;
int reshuffle_count = 0;
while(copy_list.size() != 0) {
size_t copy_list_size = copy_list.size();
2026-05-14 17:06:59 +05:30
for(auto item = copy_list.begin(); item != copy_list.end();) {
if(item->after.size() == 0) {
2026-05-14 15:27:08 +05:30
std::shared_ptr<AppInfo> app_info = std::make_shared<AppInfo>();
app_info->copyFromData(*item);
after_list.push_back(std::move(app_info));
out << "Module has no dependency: " << item->module_name << std::endl;
reshuffle_count = 0;
item = copy_list.erase(item);
}
else if(listContainsAfter(after_list, *item)) {
out << "Module: " << item->module_name <<
" has dependency: " << item->getAfterString() << " and it was found." << std::endl;
std::shared_ptr<AppInfo> app_info = std::make_shared<AppInfo>();
app_info->copyFromData(*item);
for(auto &[key, value]: item->after) {
bool found = false;
for(int i=0;i<after_list.size();i++) {
if(tolower(string_strip(after_list[i]->getModuleName())) == tolower(string_strip(key))) {
if(value == "running") {
app_info->insertParent(after_list[i], AppInfo::State::running);
}
else if(value == "exited") {
app_info->insertParent(after_list[i], AppInfo::State::exited);
}
else {
out << "Unknown state for: " << item->module_name <<
"'s dependency of: " << after_list[i]->getModuleName() << std::endl;
return std::nullopt;
}
found = true;
break;
}
}
if(!found) {
out << "Dependency not found" << std::endl;
2026-05-14 15:27:08 +05:30
}
}
after_list.push_back(std::move(app_info));
reshuffle_count = 0;
item = copy_list.erase(item);
}
2026-05-14 17:06:59 +05:30
else {
item++;
}
2026-05-14 15:27:08 +05:30
if(copy_list.size() == 0 || item == copy_list.end())
break;
}
if(copy_list.size() == copy_list_size) {
reshuffle_count++;
if(reshuffle_count >= 3) {
out << "List contains cyclic dependency or unresolvable dependency..." << std::endl;
out << "Affected apps in list: " << std::endl;
for(auto &i: copy_list) {
i.printInfo(out);
}
std::cout << "Resolved list order: " << std::endl;
printListModules(after_list, out);
return std::nullopt;
}
}
}
bool all_valid = true;
2026-05-14 15:27:08 +05:30
for(auto &i: after_list) {
bool run_validity = i->checkValidity();
i->printInfo(out);
2026-05-14 15:27:08 +05:30
for(auto &[j, state]: i->getParents()) {
if(state == AppInfo::State::exited && j->getRunMode() == AppInfo::RunMode::daemon) {
out << "[WARN]: " << i->getModuleName() <<
" is waiting for a daemon: " << j->getModuleName() << " to start execution." <<
2026-05-14 15:27:08 +05:30
std::endl;
}
}
if(!run_validity) {
out << "[ERR]: " << i->getModuleName() <<
" incorrect format for scheduled run. Please see help page." << std::endl;
all_valid = false;
} else if((i->getScheduled()) && (!i->getRunOnStart())) {
const auto* zone = std::chrono::current_zone();
auto local_now = zone->to_local(i->getNextTimeToRun());
out << "Module: " << i->getModuleName() <<
" is scheduled for execution at: " <<
std::format("{:%F %T}", local_now) << std::endl;
} else if((i->getScheduled()) && (i->getRunOnStart())) {
const auto* zone = std::chrono::current_zone();
auto local_now = zone->to_local(i->getNextTimeToRun());
out << "Module: " << i->getModuleName() <<
" will run on init and is also scheduled for execution at: " <<
std::format("{:%F %T}", local_now) << std::endl;
}
else {
out << "Module: " << i->getModuleName() <<
" is to be launched on init." << std::endl;
}
2026-05-14 15:27:08 +05:30
}
if(!all_valid) {
return std::nullopt;
}
2026-05-14 15:27:08 +05:30
return 0;
2025-09-20 20:20:47 +05:30
}
int main(int argc, char *argv[]) {
2026-05-14 15:27:08 +05:30
parseConfig();
parseArgs(argc, argv);
if(!std::filesystem::exists(options.log_directory)) {
std::filesystem::create_directories(options.log_directory);
2026-05-14 15:27:08 +05:30
}
std::filesystem::path json_path = options.json_file_path;
std::string log_file_path = std::filesystem::path(options.log_directory) / "StartUpRunner.log";
Log log_file(log_file_path, options.log_file_max_size);
std::cout << "Starter Init: Log file path: " << log_file_path << std::endl;
std::cout << "Log file open: " << log_file.isOpen() << std::endl;
2026-05-14 15:49:52 +05:30
log_file << "-------- Starting app --------" << std::endl;
log_file << "Current Time: " << get_current_time();
log_file << "Log file max size: " << options.log_file_max_size << std::endl;
2026-05-14 15:27:08 +05:30
log_file << "Current path: " << std::filesystem::current_path() << std::endl;
std::string json_file_path = json_path;
if(std::filesystem::is_directory(json_path)) {
json_file_path = json_path / "startup_apps.json";
}
else if(json_path == "") {
json_file_path = "startup_apps.json";
}
if(!std::filesystem::exists(json_file_path)) {
log_file << std::vformat("startup_apps.json not found exiting at: {}.\n", std::make_format_args(json_file_path));
exit(-1);
}
else {
log_file << "startup_apps.json loaded from: " << json_file_path << std::endl;
}
std::string data = "";
std::string t_data = "";
std::fstream json_file;
json_file.open(json_file_path, std::fstream::in);
if(!json_file.is_open()) {
log_file << "Why is the startup_apps.json file not open?" << std::endl;
std::cerr << "startup_apps.json file is not readable from location: " << json_file_path << "." << std::endl;
2026-05-14 15:27:08 +05:30
return -1;
}
std::stringstream test;
test << json_file.rdbuf();
json_file.close();
using json = nlohmann::json;
bool valid = json::accept(test.str());
if(!valid) {
log_file << "Json Document Parsing error...." << std::endl;
std::cerr << "Invalid JSON format in: " << json_file_path << "." << std::endl;
2026-05-14 15:27:08 +05:30
return -1;
}
json file = json::parse(test.str());
std::deque<AppInfo::AppData> initial_app_list;
for(auto i = file.begin(); i != file.end(); i++) {
AppInfo::AppData app_info;
auto object = i->get<json>();
if(object.find("app_name") == object.end()) {
log_file << "Error in json." << std::endl;
log_file << "Field app name is a required field." << std::endl;
std::cerr << "Field error in: " << json_file_path << ", check log for more details." << std::endl;
2026-05-14 15:27:08 +05:30
exit(-2);
}
if(object.find("name") == object.end()) {
log_file << "Error in json." << std::endl;
log_file << "Field name is a required field." << std::endl;
std::cerr << "Field error in: " << json_file_path << ", check log for more details." << std::endl;
2026-05-14 15:27:08 +05:30
exit(-2);
}
if(object.find("run_mode") == object.end()) {
log_file << "Error in json." << std::endl;
log_file << "Field run_mode is a required field." << std::endl;
std::cerr << "Field error in: " << json_file_path << ", check log for more details." << std::endl;
2026-05-14 15:27:08 +05:30
exit(-2);
}
app_info.program_name = object["app_name"].get<std::string>();
std::string args = "";
std::deque<const char*> c_args;
std::deque<std::string> arg_list;
if(object.find("app_args") != object.end()) {
for(const auto &k: object["app_args"].get<json>()) {
std::string str = k.get<std::string>();
args += str + " ";
arg_list.push_back(str);
c_args.push_back(str.c_str());
}
}
std::unordered_map<std::string, std::string> after_list;
std::unordered_map<std::string, std::string> env_list;
2026-05-14 15:27:08 +05:30
if(object.find("after") != object.end()) {
for(const auto &k: object["after"].get<json>()) {
auto l = k.get<json>();
auto key = l["module"].get<std::string>();
auto value = l["state"].get<std::string>();
after_list[key] = value;
}
}
if(object.find("env") != object.end()) {
json k = object["env"].get<json>();
for (auto &it: k.items()) {
env_list[it.key()] = it.value();
}
}
2026-05-14 15:27:08 +05:30
app_info.args = arg_list;
if(object.find("working_dir") != object.end())
app_info.working_dir = object["working_dir"].get<std::string>();
if(object.find("create_log") != object.end())
app_info.create_log = object["create_log"].get<bool>();
if(object.find("run_as") != object.end())
app_info.run_as = object["run_as"].get<std::string>();
if(object.find("run_at") != object.end())
app_info.run_at = object["run_at"].get<std::string>();
if(object.find("run_every") != object.end())
app_info.run_every = object["run_every"].get<std::string>();
if(object.find("run_on_start") != object.end())
app_info.run_on_start = object["run_on_start"].get<bool>();
app_info.env = env_list;
2026-05-14 15:27:08 +05:30
app_info.after = after_list;
app_info.module_name = object["name"].get<std::string>();
app_info.run_mode = object["run_mode"].get<std::string>();
app_info.printInfo(log_file.rdbuf());
initial_app_list.push_back(app_info);
2025-09-20 20:20:47 +05:30
}
2026-05-14 15:27:08 +05:30
std::vector<std::shared_ptr<AppInfo>> list;
log_file << "Checking App List for errors." << std::endl;
std::optional<int> list_check;
if(options.check_for_errors)
list_check = checkAppList(initial_app_list, list, std::cerr);
else
list_check = checkAppList(initial_app_list, list, log_file.rdbuf());
2026-05-14 15:27:08 +05:30
if(list_check.has_value()) {
log_file << "No errors found in app list." << std::endl;
2025-09-20 20:20:47 +05:30
}
2026-05-14 15:27:08 +05:30
else {
std::cerr << "Dependency error in: " << json_file_path << "check log for more details." << std::endl;
2026-05-14 15:27:08 +05:30
log_file << "Errors present in app list; Please fix them" << std::endl;
return -1;
}
if(options.check_for_errors)
return 0;
2026-05-14 15:27:08 +05:30
for(int i=0;i<list.size();i++) {
list[i]->printInfo(log_file.rdbuf());
}
log_file << "Order of Execution -> " << std::endl;
printListModules(list, log_file.rdbuf());
ExecutionManager manager{options.threads};
manager.setLogFile(log_file);
manager.setLogFileMaxSize(options.log_file_max_size);
manager.setLogDirectoy(options.log_directory);
manager.setJobs(list);
manager.run();
return 0;
2025-09-20 20:20:47 +05:30
}