Compare commits

...
6 changed files with 780 additions and 111 deletions

View file

@ -1,16 +1,35 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.30)
project(StartUpRunner VERSION 1.0.0 LANGUAGES CXX) project(StartUpRunner VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD 26)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(
absl
GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git
GIT_TAG 20260526.0 # Use a stable version tag or commit hash
)
FetchContent_MakeAvailable(absl)
set(RE2_BUILD_TESTING OFF CACHE BOOL "" FORCE)
set(RE2_INSTALL OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
re2
GIT_REPOSITORY https://github.com/google/re2.git
GIT_TAG 2025-11-05 # Use a stable version tag or 'main'
)
FetchContent_MakeAvailable(re2)
find_package(Boost CONFIG) find_package(Boost CONFIG)
find_package(nlohmann_json CONFIG) find_package(nlohmann_json CONFIG)
add_executable(StartUpRunner main.cpp add_executable(StartUpRunner main.cpp
appinfo.h appinfo.cpp appinfo.h appinfo.cpp
executionmanager.h executionmanager.cpp executionmanager.h executionmanager.cpp
log.h log.cpp) log.h log.cpp)
target_link_libraries(StartUpRunner PRIVATE nlohmann_json::nlohmann_json) target_link_libraries(StartUpRunner PRIVATE nlohmann_json::nlohmann_json re2::re2)

View file

@ -2,6 +2,7 @@
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
#include <re2/re2.h>
AppInfo::~AppInfo() { AppInfo::~AppInfo() {
std::cout << "Module: " << getModuleName() << " finished ops." << std::endl; std::cout << "Module: " << getModuleName() << " finished ops." << std::endl;
@ -57,6 +58,11 @@ void AppInfo::setRunAs(std::string run_as)
this->run_as = run_as; this->run_as = run_as;
} }
void AppInfo::setScheduled(bool val)
{
this->scheduled = val;
}
void AppInfo::setFinished() void AppInfo::setFinished()
{ {
this->state = State::exited; this->state = State::exited;
@ -67,6 +73,11 @@ void AppInfo::setRunning()
this->state = State::running; this->state = State::running;
} }
void AppInfo::addToPreviousExecutionList(pid_t new_process)
{
this->previous_execution_list.push_back(new_process);
}
void AppInfo::insertParent(std::shared_ptr<AppInfo> parent, State state) void AppInfo::insertParent(std::shared_ptr<AppInfo> parent, State state)
{ {
this->parents[parent] = state; this->parents[parent] = state;
@ -115,9 +126,10 @@ std::string AppInfo::getAfterString() const
for(auto &[key,value]: after) { for(auto &[key,value]: after) {
ss << key << "[" << value << "], "; ss << key << "[" << value << "], ";
} }
std::string after = ss.str(); std::string after_str = ss.str();
after = after.substr(0,after.size() - 2); if(after_str.size() >= 2)
return after; after_str = after_str.substr(0, after_str.size() - 2);
return after_str;
} }
std::string AppInfo::getAfterKeys() const std::string AppInfo::getAfterKeys() const
@ -126,9 +138,15 @@ std::string AppInfo::getAfterKeys() const
for(auto &[key,value]: after) { for(auto &[key,value]: after) {
ss << key << ", "; ss << key << ", ";
} }
std::string after = ss.str(); std::string after_str = ss.str();
after = after.substr(0,after.size() - 2); if(after_str.size() >= 2)
return after; after_str = after_str.substr(0, after_str.size() - 2);
return after_str;
}
std::string AppInfo::getStringRunMode() const
{
return runmodeToString();
} }
AppInfo::RunMode AppInfo::getRunMode() const AppInfo::RunMode AppInfo::getRunMode() const
@ -152,6 +170,9 @@ void AppInfo::copyFromData(AppInfo::AppData &other)
} }
this->after = other.after; this->after = other.after;
this->env = other.env; this->env = other.env;
this->run_at = other.run_at;
this->run_every = other.run_every;
this->run_on_start = other.run_on_start;
setRunMode(other.run_mode); setRunMode(other.run_mode);
setRunAs(other.run_as); setRunAs(other.run_as);
} }
@ -171,6 +192,26 @@ bool AppInfo::getCreateLog()
return create_log; return create_log;
} }
bool AppInfo::getScheduled()
{
return this->scheduled;
}
bool AppInfo::getRunOnStart()
{
return this->run_on_start;
}
std::vector<pid_t> AppInfo::getPreviousExecutionList() const
{
return this->previous_execution_list;
}
std::chrono::system_clock::time_point AppInfo::getNextTimeToRun() const
{
return this->next_time_to_run;
}
std::deque<std::string> AppInfo::getArgs() const std::deque<std::string> AppInfo::getArgs() const
{ {
return args; return args;
@ -244,9 +285,371 @@ void AppInfo::printInfo(std::ostream &out)
out << "Environment: " << env << std::endl; out << "Environment: " << env << std::endl;
out << "Create Log: " << create_log << std::endl; out << "Create Log: " << create_log << std::endl;
out << "Run as: " << run_as << std::endl; out << "Run as: " << run_as << std::endl;
out << "Run on start: " << run_on_start << std::endl;
const auto* zone = std::chrono::current_zone();
auto local_now = zone->to_local(this->next_time_to_run);
auto local_now_string = std::format("{:%F %T}", local_now);
if((run_at.size() != 0 || run_every.size() != 0)) {
out << "Next run scheduled at: " << local_now_string << std::endl;
} else {
out << "Normal scheduled run." << std::endl;
}
out << "************************************End************************************" << std::endl; out << "************************************End************************************" << std::endl;
} }
bool AppInfo::checkValidity()
{
// The json is wrong is run on start is set when run_every and run_at are not set
if(this->run_every.size() == 0 && this->run_at.size() == 0 && this->run_on_start)
return false;
bool got_run_every = false;
bool got_run_at = false;
// The json is wrong if the format of run_every is wrong
std::chrono::system_clock::time_point every_time_point, at_time_point;
if(this->run_every.size() != 0) {
auto next_time = parseEveryTimeFormat(run_every);
if(next_time.has_value()) {
every_time_point = next_time.value();
got_run_every = true;
}
else {
return false;
}
}
// The json is wrong if the format of run_at is wrong
if(this->run_at.size() != 0) {
auto next_time = parseAtTimeFormat(run_at);
if(next_time.has_value()) {
at_time_point = next_time.value();
got_run_at = true;
}
else {
return false;
}
}
// Update next time to run
if(got_run_every && !got_run_at) {
scheduled = true;
next_time_to_run = every_time_point;
}
if(!got_run_every && got_run_at) {
scheduled = true;
next_time_to_run = at_time_point;
}
if(got_run_every && got_run_at) {
scheduled = true;
next_time_to_run = std::min(every_time_point, at_time_point);
}
return true;
}
void AppInfo::updateNextTimeToRun()
{
next_time_to_run_parse_failed = false;
bool got_run_every = false;
bool got_run_at = false;
std::chrono::system_clock::time_point every_time_point, at_time_point;
if(this->run_every.size() != 0) {
auto next_time = parseEveryTimeFormat(run_every);
if(next_time.has_value()) {
every_time_point = next_time.value();
got_run_every = true;
}
else {
next_time_to_run_parse_failed = true;
}
}
if(this->run_at.size() != 0) {
auto next_time = parseAtTimeFormat(run_at);
if(next_time.has_value()) {
at_time_point = next_time.value();
got_run_at = true;
}
else {
next_time_to_run_parse_failed = true;
}
}
if(next_time_to_run_parse_failed)
return;
if(got_run_every && !got_run_at)
next_time_to_run = every_time_point;
if(!got_run_every && got_run_at)
next_time_to_run = at_time_point;
if(got_run_every && got_run_at)
next_time_to_run = std::min(every_time_point, at_time_point);
scheduled = true;
}
std::optional<std::chrono::system_clock::time_point> AppInfo::parseAtTimeFormat(std::string run_every)
{
/* format 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.
*/
auto timeParseDaily = [this](std::string temp_run_every, std::chrono::system_clock::time_point now) -> std::optional<std::chrono::system_clock::time_point> {
using timepoint = std::chrono::system_clock::time_point;
std::vector<timepoint> possible_time_points;
std::vector<std::string> time_list = tokenize(temp_run_every, ",");
const auto* zone = std::chrono::current_zone();
auto local_now = zone->to_local(now);
std::string date_part = std::format("{:%F}", local_now);
for(auto &time: time_list) {
std::string string_time_next = date_part + " " + time;
std::chrono::local_seconds local_next;
std::istringstream is{string_time_next};
is >> std::chrono::parse("%F %T", local_next);
if(is.fail()) {
return std::nullopt;
}
if(local_now >= local_next) {
local_next = local_next + std::chrono::days(1);
}
timepoint next_time_point = zone->to_sys(local_next, std::chrono::choose::earliest);
possible_time_points.push_back(next_time_point);
}
if(possible_time_points.size() == 0) {
return std::nullopt;
}
return *std::ranges::min_element(possible_time_points);
};
auto timeParseWeekly = [this](std::string temp_run_every, std::string week_part, std::chrono::system_clock::time_point now) -> std::optional<std::chrono::system_clock::time_point> {
using timepoint = std::chrono::system_clock::time_point;
std::vector<timepoint> possible_time_points;
std::vector<std::string> time_list = tokenize(temp_run_every, ",");
std::vector<std::string> week_list = tokenize(week_part, ",");
const auto* zone = std::chrono::current_zone();
auto local_now = zone->to_local(now);
int weekday_of_local_now = integer(std::format("{:%w}", local_now));
auto getDayOfWeekInteger = [this](std::string day) -> std::optional<int> {
if(tolower(day) == "sun") {
return 0;
}
else if(tolower(day) == "mon") {
return 1;
}
else if(tolower(day) == "tue") {
return 2;
}
else if(tolower(day) == "wed") {
return 3;
}
else if(tolower(day) == "thu") {
return 4;
}
else if(tolower(day) == "fri") {
return 5;
}
else if(tolower(day) == "sat") {
return 6;
}
else {
return std::nullopt;
}
};
for(auto &day_of_week: week_list) {
auto temp_local_now = local_now;
auto day_of_week_integer_ret = getDayOfWeekInteger(day_of_week);
if(!day_of_week_integer_ret.has_value()) {
return std::nullopt;
}
int day_of_week_integer = day_of_week_integer_ret.value();
if(day_of_week_integer != weekday_of_local_now) {
temp_local_now = temp_local_now + std::chrono::days( (day_of_week_integer - weekday_of_local_now + 7) % 7 );
}
std::string date_part = std::format("{:%F}", temp_local_now);
for(auto &time: time_list) {
std::string string_time_next = date_part + " " + time;
std::chrono::local_seconds local_next;
std::istringstream is{string_time_next};
is >> std::chrono::parse("%F %T", local_next);
if(is.fail()) {
return std::nullopt;
}
if(temp_local_now >= local_next && day_of_week_integer == weekday_of_local_now) {
local_next = local_next + std::chrono::days(7);
}
timepoint next_time_point = zone->to_sys(local_next, std::chrono::choose::earliest);
possible_time_points.push_back(next_time_point);
}
}
if(possible_time_points.size() == 0) {
return std::nullopt;
}
return *std::ranges::min_element(possible_time_points);
};
auto timeParseMonthly = [this](std::string temp_run_every, std::string month_part, std::chrono::system_clock::time_point now) -> std::optional<std::chrono::system_clock::time_point> {
using timepoint = std::chrono::system_clock::time_point;
std::vector<timepoint> possible_time_points;
std::vector<std::string> time_list = tokenize(temp_run_every, ",");
std::vector<std::string> month_days_list = tokenize(month_part, ",");
const auto* zone = std::chrono::current_zone();
auto local_now = zone->to_local(now);
std::chrono::year_month_day current_ymd{std::chrono::floor<std::chrono::days>(local_now)};
for(auto &day_of_month: month_days_list) {
int day_int = integer(day_of_month);
if (day_int < 1 || day_int > 31) {
return std::nullopt;
}
std::chrono::day target_day{static_cast<unsigned>(day_int)};
std::chrono::year_month_day ymd = current_ymd.year() / current_ymd.month() / target_day;
if (!ymd.ok()) {
ymd = ymd.year() / ymd.month() / std::chrono::last;
}
for(auto &time: time_list) {
std::chrono::seconds time_of_day;
std::istringstream is{time};
is >> std::chrono::parse("%T", time_of_day);
if (is.fail()) {
return std::nullopt;
}
auto local_next = std::chrono::local_days{ymd} + time_of_day;
if (local_next <= local_now) {
auto next_ym = current_ymd.year() / current_ymd.month() + std::chrono::months(1);
std::chrono::year_month_day ymd_next = next_ym / target_day;
if (!ymd_next.ok()) {
ymd_next = next_ym / std::chrono::last;
}
local_next = std::chrono::local_days{ymd_next} + time_of_day;
}
timepoint next_time_point = zone->to_sys(local_next, std::chrono::choose::earliest);
possible_time_points.push_back(next_time_point);
}
}
if(possible_time_points.size() == 0) {
return std::nullopt;
}
return *std::ranges::min_element(possible_time_points);
};
using timepoint = std::chrono::system_clock::time_point;
std::chrono::time_point now = std::chrono::system_clock::now();
if(run_every.contains("Daily")) {
std::string temp_run_every = run_every.erase(run_every.find("Daily"));
temp_run_every = stringStrip(temp_run_every);
auto options = timeParseDaily(temp_run_every, now);
if(!options.has_value()) {
return std::nullopt;
}
return options.value();
}
else if(run_every.contains("Weekly")) {
std::string temp_run_weekly = run_every.erase(run_every.find("Weekly"));
temp_run_weekly = stringStrip(temp_run_weekly);
std::string week_part;
std::string time_part;
if(!RE2::FullMatch(temp_run_weekly,R"(([a-zA-Z ,]+)([0-9: ,]+))", &week_part, &time_part)) {
return std::nullopt;
}
week_part = stringStrip(week_part);
time_part = stringStrip(time_part);
auto options = timeParseWeekly(time_part, week_part, now);
if(!options.has_value()) {
return std::nullopt;
}
return options.value();
}
else if(run_every.contains("Monthly")) {
std::string temp_run_monthly = run_every.erase(run_every.find("Monthly"));
temp_run_monthly = stringStrip(temp_run_monthly);
std::string_view view_for_temp_run_monthy(temp_run_monthly);
std::string temp_time_part;
std::string time_part = "";
re2::RE2 r(R"((\d\d:\d\d:\d\d[, ]?)+)");
while (RE2::FindAndConsume(&view_for_temp_run_monthy, r, &temp_time_part)) {
time_part += temp_time_part;
}
RE2::GlobalReplace(&temp_run_monthly,R"((\d\d:\d\d:\d\d[, ]?)+)", "");
std::string month_part = stringStrip(temp_run_monthly); time_part = stringStrip(time_part);
auto options = timeParseMonthly(time_part, month_part, now);
if(!options.has_value()) {
return std::nullopt;
}
return options.value();
}
else {
return std::nullopt;
}
}
std::optional<std::chrono::system_clock::time_point> AppInfo::parseEveryTimeFormat(std::string run_every)
{
/* 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
*/
int duration = 0;
std::string unit;
if(!RE2::FullMatch(run_every,R"((\d+)([sSmMhHdDwW]))", &duration, &unit)) {
return std::nullopt;
}
if(duration <= 0) {
return std::nullopt;
}
if(unit.size() <= 0) {
return std::nullopt;
}
char ch = unit[0];
std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
const auto* zone = std::chrono::current_zone();
auto local_now = zone->to_local(now);
switch(ch) {
case 's':
case 'S': {
return zone->to_sys(local_now + std::chrono::seconds(duration), std::chrono::choose::earliest);
}
case 'm': {
return zone->to_sys(local_now + std::chrono::minutes(duration), std::chrono::choose::earliest);
}
case 'h':
case 'H': {
return zone->to_sys(local_now + std::chrono::hours(duration), std::chrono::choose::earliest);
}
case 'd':
case 'D': {
return zone->to_sys(local_now + std::chrono::days(duration), std::chrono::choose::earliest);
}
case 'w':
case 'W': {
return zone->to_sys(local_now + std::chrono::weeks(duration), std::chrono::choose::earliest);
}
case 'M': {
auto today = std::chrono::floor<std::chrono::days>(local_now);
auto time_of_day = local_now - today;
std::chrono::year_month_day ymd{today};
ymd += std::chrono::months(duration);
if (!ymd.ok()) {
ymd = ymd.year() / ymd.month() / std::chrono::last;
}
auto next_local = std::chrono::local_days{ymd} + time_of_day;
return zone->to_sys(next_local, std::chrono::choose::earliest);
}
default: {
return std::nullopt;
}
}
}
std::string AppInfo::runmodeToString() const std::string AppInfo::runmodeToString() const
{ {
switch(this->mode) { switch(this->mode) {
@ -259,6 +662,67 @@ std::string AppInfo::runmodeToString() const
} }
} }
std::vector<std::string> AppInfo::tokenize(std::string main_string, std::string sub_string)
{
std::vector<std::string> tokens;
size_t pos = 0;
while ((pos = main_string.find(sub_string)) != std::string::npos) {
std::string token = stringStrip(main_string.substr(0, pos));
if (!token.empty()) {
tokens.push_back(token);
}
main_string.erase(0, pos + sub_string.length());
}
std::string last_token = stringStrip(main_string);
if (!last_token.empty()) {
tokens.push_back(last_token);
}
return tokens;
}
std::string AppInfo::stringStrip(const std::string str)
{
std::string ch;
bool front_found = false;
for(int i=0; i<str.size(); i++) {
if(!front_found) {
if(str[i] == ' ' || str[i] == '\t' || str[i] == '\n' || str[i] == '\v' || str[i] == '\r')
continue;
else {
front_found = true;
ch += str[i];
continue;
}
}
else
ch += str[i];
}
auto pos = ch.find_last_not_of(" \t\n\v\r");
if(pos == std::string::npos)
return ch;
ch.erase(pos+1);
return ch;
}
int AppInfo::integer(const std::string str)
{
std::stringstream ss;
int i = -1;
ss << str;
if (!(ss >> i) || !(ss >> std::ws).eof())
return -1;
return i;
}
std::string AppInfo::tolower(const std::string str) const
{
std::string s;
std::transform(str.begin(), str.end(), std::back_inserter(s), [](unsigned char c){
return static_cast<char>(std::tolower(c));
});
return s;
}
std::string AppInfo::stateToString() const std::string AppInfo::stateToString() const
{ {
return stateToString(this->state); return stateToString(this->state);
@ -309,6 +773,9 @@ void AppInfo::AppData::printInfo(std::ostream &out)
out << "Environment: " << env << std::endl; out << "Environment: " << env << std::endl;
out << "Create Log: " << AppData::create_log << std::endl; out << "Create Log: " << AppData::create_log << std::endl;
out << "Run as: " << run_as << std::endl; out << "Run as: " << run_as << std::endl;
out << "Run on start: " << run_on_start << std::endl;
out << "Run At: " << run_at << std::endl;
out << "Run Every: " << run_every << std::endl;
out << "************************************End************************************" << std::endl; out << "************************************End************************************" << std::endl;
} }
@ -318,9 +785,10 @@ std::string AppInfo::AppData::getAfterString() const
for(auto &[key,value]: after) { for(auto &[key,value]: after) {
ss << key << "[" << value << "], "; ss << key << "[" << value << "], ";
} }
std::string after = ss.str(); std::string after_str = ss.str();
after = after.substr(0,after.size() - 2); if(after_str.size() >= 2)
return after; after_str = after_str.substr(0, after_str.size() - 2);
return after_str;
} }
std::string AppInfo::AppData::getAfterKeys() const std::string AppInfo::AppData::getAfterKeys() const
@ -329,7 +797,8 @@ std::string AppInfo::AppData::getAfterKeys() const
for(auto &[key,value]: after) { for(auto &[key,value]: after) {
ss << key << ", "; ss << key << ", ";
} }
std::string after = ss.str(); std::string after_str = ss.str();
after = after.substr(0,after.size() - 2); if(after_str.size() >= 2)
return after; after_str = after_str.substr(0, after_str.size() - 2);
return after_str;
} }

View file

@ -8,6 +8,8 @@
#include <unordered_map> #include <unordered_map>
#include <syncstream> #include <syncstream>
#include <atomic> #include <atomic>
#include <format>
#include <chrono>
class AppInfo class AppInfo
{ {
@ -32,6 +34,9 @@ public:
std::string run_mode; std::string run_mode;
std::string run_as; std::string run_as;
bool create_log = false; bool create_log = false;
std::string run_at;
std::string run_every;
bool run_on_start = false;
void printInfo(std::ostream &out); void printInfo(std::ostream &out);
std::string getAfterString() const; std::string getAfterString() const;
std::string getAfterKeys() const; std::string getAfterKeys() const;
@ -43,12 +48,19 @@ private:
std::deque<std::string> args; std::deque<std::string> args;
std::string run_as; std::string run_as;
std::string working_dir; std::string working_dir;
std::string run_at;
std::string run_every;
bool run_on_start = false;
string_map after; string_map after;
string_map env; string_map env;
bool create_log = false; bool create_log = false;
std::unordered_map<std::shared_ptr<AppInfo>, State> parents; std::unordered_map<std::shared_ptr<AppInfo>, State> parents;
std::atomic<State> state{State::initial}; std::atomic<State> state{State::initial};
RunMode mode = RunMode::script; RunMode mode = RunMode::script;
std::chrono::system_clock::time_point next_time_to_run;
bool next_time_to_run_parse_failed = false;
bool scheduled = false;
std::vector<pid_t> previous_execution_list;
public: public:
void setProgramName(std::string name); void setProgramName(std::string name);
void setModuleName(std::string name); void setModuleName(std::string name);
@ -59,8 +71,10 @@ public:
void setCreateLog(bool create_log); void setCreateLog(bool create_log);
void setRunMode(std::string mode); void setRunMode(std::string mode);
void setRunAs(std::string run_as); void setRunAs(std::string run_as);
void setScheduled(bool val);
void setFinished(); void setFinished();
void setRunning(); void setRunning();
void addToPreviousExecutionList(pid_t new_process);
void insertParent(std::shared_ptr<AppInfo> parent, State state); void insertParent(std::shared_ptr<AppInfo> parent, State state);
bool isParentDependencyCleared(std::osyncstream &out) const; bool isParentDependencyCleared(std::osyncstream &out) const;
std::string getModuleName() const; std::string getModuleName() const;
@ -73,10 +87,15 @@ public:
std::string getAfterString() const; std::string getAfterString() const;
std::string getAfterKeys() const; std::string getAfterKeys() const;
RunMode getRunMode() const; RunMode getRunMode() const;
std::string getStringRunMode() const;
State getState() const; State getState() const;
std::unordered_map<std::shared_ptr<AppInfo>, State> getParents() const; std::unordered_map<std::shared_ptr<AppInfo>, State> getParents() const;
void copyFromData(AppInfo::AppData &other); void copyFromData(AppInfo::AppData &other);
bool getCreateLog(); bool getCreateLog();
bool getScheduled();
bool getRunOnStart();
std::vector<pid_t> getPreviousExecutionList() const;
std::chrono::system_clock::time_point getNextTimeToRun() const;
AppInfo(); AppInfo();
~AppInfo(); ~AppInfo();
AppInfo(const AppInfo &other) = delete; AppInfo(const AppInfo &other) = delete;
@ -84,8 +103,16 @@ public:
AppInfo &operator=(const AppInfo &other) = delete; AppInfo &operator=(const AppInfo &other) = delete;
bool operator==(const AppInfo &other); bool operator==(const AppInfo &other);
void printInfo(std::ostream& out); void printInfo(std::ostream& out);
bool checkValidity();
void updateNextTimeToRun();
private: private:
std::optional<std::chrono::system_clock::time_point> parseAtTimeFormat(std::string run_every);
std::optional<std::chrono::system_clock::time_point> parseEveryTimeFormat(std::string run_every);
std::string runmodeToString() const; std::string runmodeToString() const;
std::vector<std::string> tokenize(std::string, std::string);
std::string stringStrip(const std::string str);
int integer(const std::string str);
std::string tolower(const std::string str) const;
std::string stateToString() const; std::string stateToString() const;
std::string stateToString(State s) const; std::string stateToString(State s) const;
}; };

View file

@ -46,44 +46,68 @@ void ExecutionManager::setJobs(std::vector<std::shared_ptr<AppInfo> > &job_list)
this->job_list = std::move(job_list); this->job_list = std::move(job_list);
} }
//This function assumes that job_list_mutex is locked
void ExecutionManager::printActiveJobs()
{
for(auto &i: job_list){
log_file << "Main Thread: " + i->getModuleName() + " mode: " + i->getStringRunMode() + " is running.";
}
}
void ExecutionManager::run() void ExecutionManager::run()
{ {
finished = false; finished = false;
initial_list_size = job_list.size(); seperateScheduledTasks();
log_file << "Main Thread: Job list: " << job_list.size() <<
" Scheduled list: " << scheduled_list.size() << std::endl;
last_poll_time = std::chrono::system_clock::now();
while(true) { while(true) {
for(int i=active_thread_count.load(); i < threads ;i++) { // Clean up completed worker threads
int val = 0; for(auto it = thread_list.begin(); it != thread_list.end(); ) {
{ if((*it)->done.load()) {
std::lock_guard<std::mutex> lock_get(this->job_list_mutex); if((*it)->thread.joinable())
val = index_count++; (*it)->thread.join();
if(job_list.empty()) { it = thread_list.erase(it);
log_file << "Main Thread: Job list is empty, waiting for finish." << std::endl; } else {
break; ++it;
}
if(val > (initial_list_size - 1)) {
log_file << "Main Thread: All jobs queued." << std::endl;
break;
} }
} }
active_thread_count.fetch_add(1);
std::shared_ptr<std::thread> thread = std::make_shared<std::thread>([this, val](){ pollScheduledTasks();
// Dispatch jobs from job_list to worker threads
while(active_thread_count.load() < threads) {
std::shared_ptr<AppInfo> ptr; std::shared_ptr<AppInfo> ptr;
std::osyncstream os(log_file.rdbuf()); int val = 0;
size_t list_size = 0; size_t remaining_size = 0;
{ {
std::lock_guard<std::mutex> lock_get(this->job_list_mutex); std::lock_guard<std::mutex> lock_get(this->job_list_mutex);
if(job_list.size() == 0) { if(job_list.empty()) {
os << std::vformat("Thread[{}] called while the job list was empty: ", std::make_format_args(val)) break;
<< std::endl;
return;
} }
ptr = this->job_list[0]; ptr = job_list.front();
job_list.erase(job_list.begin()); job_list.erase(job_list.begin());
list_size = job_list.size(); remaining_size = job_list.size();
val = index_count++;
} }
active_thread_count.fetch_add(1);
auto worker = std::make_shared<WorkerThread>();
worker->thread = std::thread([this, val, ptr, remaining_size, worker](){
struct ThreadGuard {
std::atomic<int>& count;
std::atomic<bool>& done;
~ThreadGuard() {
count.fetch_sub(1);
done.store(true);
}
} guard{active_thread_count, worker->done};
std::osyncstream os(log_file.rdbuf());
std::string module_name = ptr->getModuleName(); std::string module_name = ptr->getModuleName();
os << std::vformat("Thread[{}] running: ", std::make_format_args(val)) os << std::vformat("Thread[{}] running: ", std::make_format_args(val))
<< module_name << " current list size: " << list_size << std::endl; << module_name << " current list size: " << remaining_size << std::endl;
int old = state_change.load(); int old = state_change.load();
while(!ptr->isParentDependencyCleared(os)) while(!ptr->isParentDependencyCleared(os))
{ {
@ -178,13 +202,21 @@ void ExecutionManager::run()
default: { default: {
// After fork in parent do // After fork in parent do
ptr->setRunning(); ptr->setRunning();
// If it was a scheduled task recalculate its next execution time
// and send it back to the scheduled job list
if(ptr->getScheduled()) {
ptr->addToPreviousExecutionList(pid);
ptr->updateNextTimeToRun();
std::lock_guard<std::mutex> lock_get(this->job_list_mutex);
scheduled_list.push_back(ptr);
}
state_change.fetch_add(1); state_change.fetch_add(1);
state_change.notify_all(); state_change.notify_all();
break; break;
} }
} }
// If script daemon wait here // If script wait here
AppInfo::RunMode mode = ptr->getRunMode(); AppInfo::RunMode mode = ptr->getRunMode();
int status; int status;
if(mode == AppInfo::RunMode::script) { if(mode == AppInfo::RunMode::script) {
@ -198,29 +230,68 @@ void ExecutionManager::run()
state_change.fetch_add(1); state_change.fetch_add(1);
state_change.notify_all(); state_change.notify_all();
os << std::vformat("Thread[{}] finished: ", std::make_format_args(val)) os << std::vformat("Thread[{}] finished: ", std::make_format_args(val))
<< module_name<< std::endl; << module_name << std::endl;
active_thread_count.fetch_sub(1);
}); });
thread_list.push_back(thread); thread_list.push_back(worker);
} }
// Check if all work is done (no active jobs, no pending jobs, no scheduled tasks)
{ {
std::lock_guard<std::mutex> lock_get(this->job_list_mutex); std::lock_guard<std::mutex> lock_get(this->job_list_mutex);
if(job_list.empty()) { if(job_list.empty() && scheduled_list.empty() && active_thread_count.load() == 0) {
log_file << "Main Thread: Job list is empty, waiting for finish." << std::endl; log_file << "Main Thread: All jobs finished." << std::endl;
break;
}
if((index_count) > (initial_list_size - 1)) {
log_file << "Main Thread: All jobs queued." << std::endl;
break; break;
} }
} }
if (active_thread_count.load() >= threads) { std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
} }
for(auto &worker: thread_list) {
if(worker->thread.joinable())
worker->thread.join();
} }
for(auto i: thread_list) { thread_list.clear();
if(i->joinable())
i->join();
}
finished = true; finished = true;
} }
bool ExecutionManager::checkScheduledTaskIsReady(std::chrono::system_clock::time_point next_time)
{
return (next_time <= std::chrono::system_clock::now());
}
void ExecutionManager::seperateScheduledTasks()
{
std::lock_guard<std::mutex> lock_get(this->job_list_mutex);
for(auto task = job_list.begin(); task != job_list.end(); ) {
// If task is not running on start and task is scheduled.
if(!((*task)->getRunOnStart()) && ((*task)->getScheduled())) {
log_file << "Main Thread: Job[" + (*task)->getModuleName() +
"] is scheduled for execution." << std::endl;
scheduled_list.push_back(*task);
task = job_list.erase(task);
} else {
++task;
}
}
}
void ExecutionManager::pollScheduledTasks()
{
auto current_poll_time = std::chrono::system_clock::now();
if(std::chrono::duration_cast<std::chrono::seconds>(current_poll_time - last_poll_time).count() < polling_interval)
return;
last_poll_time = current_poll_time;
{
std::lock_guard<std::mutex> lock_get(this->job_list_mutex);
for(auto task = scheduled_list.begin(); task != scheduled_list.end(); ) {
if(checkScheduledTaskIsReady((*task)->getNextTimeToRun())) {
log_file << "Main Thread: Job[" + (*task)->getModuleName() +
"] scheduled time arrived, adding to execution queue." << std::endl;
job_list.push_back(*task);
task = scheduled_list.erase(task);
} else {
++task;
}
}
}
}

View file

@ -10,17 +10,23 @@
class ExecutionManager class ExecutionManager
{ {
struct WorkerThread {
std::thread thread;
std::atomic<bool> done{false};
};
bool finished = false; bool finished = false;
int threads; int threads;
int index_count = 0; int index_count = 0;
int initial_list_size = 0;
long long int log_file_max_size = 0; long long int log_file_max_size = 0;
std::string log_dir; std::string log_dir;
std::mutex job_list_mutex; std::mutex job_list_mutex;
std::atomic<int> state_change = 0; std::atomic<int> state_change = 0;
std::atomic<int> active_thread_count = 0; std::atomic<int> active_thread_count = 0;
std::vector<std::shared_ptr<std::thread>> thread_list; std::vector<std::shared_ptr<WorkerThread>> thread_list;
std::vector<std::shared_ptr<AppInfo>> job_list; std::vector<std::shared_ptr<AppInfo>> job_list;
std::vector<std::shared_ptr<AppInfo>> scheduled_list;
std::chrono::system_clock::time_point last_poll_time;
int polling_interval = 1;
Log log_file; Log log_file;
public: public:
ExecutionManager(int threads=16); ExecutionManager(int threads=16);
@ -28,7 +34,12 @@ public:
void setLogFileMaxSize(long long int size); void setLogFileMaxSize(long long int size);
void setLogDirectoy(std::string log_dir); void setLogDirectoy(std::string log_dir);
void setJobs(std::vector<std::shared_ptr<AppInfo>>& job_list); void setJobs(std::vector<std::shared_ptr<AppInfo>>& job_list);
void printActiveJobs();
void run(); void run();
private:
bool checkScheduledTaskIsReady(std::chrono::system_clock::time_point next_time);
void seperateScheduledTasks();
void pollScheduledTasks();
}; };
#endif // EXECUTIONMANAGER_H #endif // EXECUTIONMANAGER_H

184
main.cpp
View file

@ -89,55 +89,81 @@ std::string strip(std::string& t) {
} }
void printHelp() { void printHelp() {
std::cout << "Usage: StartUpRunner [OPTIONS]" << std::endl; std::cout << R"(Usage: StartUpRunner [OPTIONS]
std::cout << "OPTIONS:" << std::endl; OPTIONS:
std::cout << "\t--help|-h : Prints this screen." << std::endl; --help|-h : Prints this screen.
std::cout << "\t--version|-v : Prints version info." << std::endl; --version|-v : Prints version info.
std::cout << "\t--threads|-t [T] : Sets number of threads to [T]." << std::endl; --threads|-t [T] : Sets number of threads to [T].
std::cout << "\t--log_dir|-ld [Dir] : Sets log directory to [Dir]." << std::endl; --log_dir|-ld [Dir] : Sets log directory to [Dir].
std::cout << "\t--defaults|-d : Prints the default values used if no arguments are provided." << std::endl; --defaults|-d : Prints the default values used if no arguments are provided.
std::cout << "\t--json|-j [FileName]: Set Json Startup List to FileName." << std::endl; --json|-j [FileName]: Set Json Startup List to FileName.
std::cout << "\t--test|-T [FileName]: Check the json file for errors." << std::endl; --test|-T [FileName]: Check the json file for errors.
std::cout << "For a detailed info about the json startup file use --help json." << std::endl; For a detailed info about the json startup file use --help json.)" << std::endl;
} }
void printVersion() { void printVersion() {
std::cout << "StartUpRunner: Version-0.0.2 Pre-Alpha Pre-BigBang Era Edition." << std::endl; std::cout << "StartUpRunner: Version-0.0.3 Pre-Alpha Pre-BigBang Era Edition." << std::endl;
} }
void printDefaults() { void printDefaults() {
std::cout << "StartUpRunner Default Values: " << std::endl; std::cout << "StartUpRunner Default Values: " << std::endl;
std::cout << "Json file : " << options.json_file_path << std::endl; std::cout << "Json file : " << options.json_file_path << std::endl;
std::cout << "Logs directory : " << options.log_directory << std::endl; std::cout << "Logs directory : " << options.log_directory << std::endl;
std::cout << "Log file max size: " << options.threads << std::endl; std::cout << "Log file max size: " << options.log_file_max_size << std::endl;
std::cout << "Threads : " << options.threads << std::endl; std::cout << "Threads : " << options.threads << std::endl;
} }
void printJsonHelp() { void printJsonHelp() {
std::cout << "Json StartUp List file format: " << std::endl; std::cout << R"(Json StartUp List file format:
std::cout << "Top level is an Array of Apps." << std::endl; Top level is an Array of Apps.
std::cout << "Each App Entry contains: " << std::endl; Each App Entry contains:
std::cout << "\t[REQUIRED] name[string] : Name of the module." << std::endl; [REQUIRED] name[string] : Name of the module.
std::cout << "\t[REQUIRED] app_name[string] : Path to the program with its name." << std::endl; [REQUIRED] app_name[string] : Path to the program with its name.
std::cout << "\t[OPTIONAL] app_args[list[string]] : List of arguments to the program." << std::endl; [REQUIRED] run_mode[string] : Mode can be daemon or script.
std::cout << "\t[REQUIRED] run_mode[string] : Mode can be daemon or script." << std::endl; [OPTIONAL] app_args[list[string]] : List of arguments to the program.
std::cout << "\t\tMode daemon : The app forks the program and exits without waiting." << std::endl; Mode daemon : The app forks the program and exits without waiting.
std::cout << "\t\tMode script : The app forks the program and waits for it to finish execution." << std::endl; Mode script : The app forks the program and waits for it to finish execution.
std::cout << "\t\t If no run_mode is given it is presumed to be script." << std::endl; If no run_mode is given it is presumed to be script.
std::cout << "\t[OPTIONAL] after[{name,state}] : List containing names and state of modules." << std::endl; [OPTIONAL] after[{name,state}] : List containing names and state of modules.
std::cout << "\t\tname[string] : Name of the other module." << std::endl; name[string] : Name of the other module.
std::cout << "\t\tstate[string]: State of the other module." << std::endl; state[string]: State of the other module.
std::cout << "\t\t\tState exited : Specified module must have finished execution." << std::endl; State exited : Specified module must have finished execution.
std::cout << "\t\t\tState running : Specified module must be running." << std::endl; State running : Specified module must be running.
std::cout << "\t\tThis module will be launched after the module found\n\t\t" This module will be launched after the module foundin this list using name is at the state specified as state.
"in this list using name is at the state specified as state." << std::endl; [OPTIONAL] env{key: value} : Additional enviroment variables.
std::cout << "\t[OPTIONAL] env{} : Additional enviroment variables." << std::endl; Every key in this object will be used as environment name,
std::cout << "\t\tEvery key in this object will be used as environment name,\n\t\tand every corresponding value as its value." << std::endl; and every corresponding value as its value.
std::cout << "\t[OPTIONAL] working_dir[string] : Working directory of the program." << std::endl; [OPTIONAL] working_dir[string] : Working directory of the program.
std::cout << "\t[OPTIONAL] create_log[boolean] : Create a log file at logs path." << std::endl; [OPTIONAL] create_log[boolean] : Create a log file at logs path.
std::cout << "\t[OPTIONAL] run_as[string] : Run as a particular user." << std::endl; [OPTIONAL] run_as[string] : Run as a particular user.
std::cout << std::endl << "Example startup_apps.json: "; [OPTIONAL] run_every[string] : Run task at every specified interval.
std::cout << R"( 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:
[ [
{ {
"name": "LS 0", "name": "LS 0",
@ -156,10 +182,24 @@ void printJsonHelp() {
"after": [{"module": "LS 0", "state": "exited"}], "after": [{"module": "LS 0", "state": "exited"}],
"env": {"PATH": "/home/:/usr/bin/", "NAME": "ONE"}, "env": {"PATH": "/home/:/usr/bin/", "NAME": "ONE"},
"working_dir": "/home/etc", "working_dir": "/home/etc",
"run_as": "username" "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"
} }
] ]
)" << std::endl; )";
} }
int contains(std::vector<std::string> &list, std::string l, std::string v) { int contains(std::vector<std::string> &list, std::string l, std::string v) {
@ -206,7 +246,7 @@ void parseArgs(int c, char *v[]) {
int i = contains(arg_list, "--json", "-j"); int i = contains(arg_list, "--json", "-j");
if(i >= 0) { if(i >= 0) {
int j = i + 1; int j = i + 1;
if(j > arg_list.size()) { if(j >= arg_list.size()) {
printHelp(); printHelp();
exit(-1); exit(-1);
} }
@ -217,7 +257,7 @@ void parseArgs(int c, char *v[]) {
i = contains(arg_list, "--log_dir", "-ld"); i = contains(arg_list, "--log_dir", "-ld");
if(i >= 0) { if(i >= 0) {
int j = i + 1; int j = i + 1;
if(j > arg_list.size()) { if(j >= arg_list.size()) {
printHelp(); printHelp();
exit(-1); exit(-1);
} }
@ -228,7 +268,7 @@ void parseArgs(int c, char *v[]) {
i = contains(arg_list, "--threads", "-t"); i = contains(arg_list, "--threads", "-t");
if(i >= 0) { if(i >= 0) {
int j = i + 1; int j = i + 1;
if(j > arg_list.size()) { if(j >= arg_list.size()) {
printHelp(); printHelp();
exit(-1); exit(-1);
} }
@ -238,29 +278,24 @@ void parseArgs(int c, char *v[]) {
} }
i = contains(arg_list, "--test", "-T"); i = contains(arg_list, "--test", "-T");
if(i >= 0) { if(i >= 0) {
int j = i + 1;
if(j > arg_list.size()) {
printHelp();
exit(-1);
}
options.check_for_errors = true; 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; std::cout << "Testing json file." << std::endl;
contains_any_known_arg = true; contains_any_known_arg = true;
} }
i = contains(arg_list, "--help", "-h"); i = contains(arg_list, "--help", "-h");
if(i >= 0) { if(i >= 0) {
int j = i + 1; int j = i + 1;
if(j > arg_list.size()) { if(j < arg_list.size() && arg_list[j] == "json") {
printHelp();
exit(-1);
}
if(arg_list[j] == "json") {
printJsonHelp(); printJsonHelp();
exit(0); exit(0);
} }
else { else {
printHelp(); printHelp();
exit(-1); exit(0);
} }
contains_any_known_arg = true; contains_any_known_arg = true;
} }
@ -299,11 +334,14 @@ std::vector<std::string> string_split(std::string s, const std::string& delimite
size_t pos = 0; size_t pos = 0;
std::string token; std::string token;
while ((pos = s.find(delimiter)) != std::string::npos) { while ((pos = s.find(delimiter)) != std::string::npos) {
token = s.substr(0, pos); token = string_strip(s.substr(0, pos));
if(!token.empty())
tokens.push_back(token); tokens.push_back(token);
s.erase(0, pos + delimiter.length()); s.erase(0, pos + delimiter.length());
} }
tokens.push_back(string_strip(s)); std::string last = string_strip(s);
if(!last.empty())
tokens.push_back(last);
return tokens; return tokens;
} }
@ -396,7 +434,7 @@ std::optional<int> checkAppList(std::deque<AppInfo::AppData> &copy_list, std::ve
} }
} }
if(!found) { if(!found) {
out << "Depenedency not found" << std::endl; out << "Dependency not found" << std::endl;
} }
} }
after_list.push_back(std::move(app_info)); after_list.push_back(std::move(app_info));
@ -423,14 +461,41 @@ std::optional<int> checkAppList(std::deque<AppInfo::AppData> &copy_list, std::ve
} }
} }
} }
bool all_valid = true;
for(auto &i: after_list) { for(auto &i: after_list) {
bool run_validity = i->checkValidity();
i->printInfo(out);
for(auto &[j, state]: i->getParents()) { for(auto &[j, state]: i->getParents()) {
if(state == AppInfo::State::exited && j->getRunMode() == AppInfo::RunMode::daemon) { if(state == AppInfo::State::exited && j->getRunMode() == AppInfo::RunMode::daemon) {
out << "[WARN]: " << i->getModuleName() << out << "[WARN]: " << i->getModuleName() <<
" is waiting for a daemon: " << j->getModuleName() << " to start exeuction." << " is waiting for a daemon: " << j->getModuleName() << " to start execution." <<
std::endl; 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;
}
}
if(!all_valid) {
return std::nullopt;
} }
return 0; return 0;
} }
@ -541,6 +606,13 @@ int main(int argc, char *argv[]) {
app_info.create_log = object["create_log"].get<bool>(); app_info.create_log = object["create_log"].get<bool>();
if(object.find("run_as") != object.end()) if(object.find("run_as") != object.end())
app_info.run_as = object["run_as"].get<std::string>(); 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; app_info.env = env_list;
app_info.after = after_list; app_info.after = after_list;
app_info.module_name = object["name"].get<std::string>(); app_info.module_name = object["name"].get<std::string>();