diff --git a/CMakeLists.txt b/CMakeLists.txt index b93f61ea9..4c8b82812 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,4 +36,6 @@ if(WEBVIEW_BUILD) if(WEBVIEW_ENABLE_PACKAGING) add_subdirectory(packaging) endif() + + add_subdirectory(alloy) endif() diff --git a/alloy/CMakeLists.txt b/alloy/CMakeLists.txt new file mode 100644 index 000000000..3aa754487 --- /dev/null +++ b/alloy/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.16) +project(alloy LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) # Updated to C++17 for std::filesystem +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(ALLOY_SOURCES + main.cpp + runtime.cpp + cron_parser.cpp + subprocess.cpp + process_manager.cpp + shell_lexer.cpp + shell_parser.cpp + shell_builtins.cpp + shell_interpreter.cpp + shell_glob.cpp +) + +if(APPLE) + list(APPEND ALLOY_SOURCES cron_manager_macos.mm) +elseif(WIN32) + list(APPEND ALLOY_SOURCES cron_manager_windows.cpp) +else() + list(APPEND ALLOY_SOURCES cron_manager_linux.cpp) +endif() + +add_executable(alloy_bin ${ALLOY_SOURCES}) +target_include_directories(alloy_bin PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../core/include) +target_link_libraries(alloy_bin PRIVATE webview::core) + +if(UNIX AND NOT APPLE) + find_package(PkgConfig REQUIRED) + pkg_check_modules(GTK3 REQUIRED gtk+-3.0 webkit2gtk-4.1) + target_include_directories(alloy_bin PRIVATE ${GTK3_INCLUDE_DIRS}) + target_link_libraries(alloy_bin PRIVATE ${GTK3_LIBRARIES} dl util) +elseif(APPLE) + target_link_libraries(alloy_bin PRIVATE "-framework WebKit" dl) +elseif(WIN32) + target_link_libraries(alloy_bin PRIVATE advapi32 ole32 shell32 shlwapi user32 version) +endif() diff --git a/alloy/cron_manager.cpp b/alloy/cron_manager.cpp new file mode 100644 index 000000000..dee6d7b01 --- /dev/null +++ b/alloy/cron_manager.cpp @@ -0,0 +1,13 @@ +#include "cron_manager.hpp" + +namespace alloy { + +#ifdef _WIN32 +// Included via cron_manager_windows.cpp in actual build +#elif defined(__APPLE__) +// Included via cron_manager_macos.mm in actual build +#else +// Included via cron_manager_linux.cpp in actual build +#endif + +} // namespace alloy diff --git a/alloy/cron_manager.hpp b/alloy/cron_manager.hpp new file mode 100644 index 000000000..a6faa5cdc --- /dev/null +++ b/alloy/cron_manager.hpp @@ -0,0 +1,16 @@ +#ifndef ALLOY_CRON_MANAGER_HPP +#define ALLOY_CRON_MANAGER_HPP + +#include + +namespace alloy { + +class cron_manager { +public: + static void register_job(const std::string& path, const std::string& schedule, const std::string& title); + static void remove_job(const std::string& title); +}; + +} // namespace alloy + +#endif // ALLOY_CRON_MANAGER_HPP diff --git a/alloy/cron_manager_linux.cpp b/alloy/cron_manager_linux.cpp new file mode 100644 index 000000000..20b72840c --- /dev/null +++ b/alloy/cron_manager_linux.cpp @@ -0,0 +1,130 @@ +#include "cron_manager.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace alloy { + +static std::string run_command_with_args(const std::vector& args, const std::string& input = "") { + int input_pipe[2]; + int output_pipe[2]; + if (pipe(input_pipe) == -1 || pipe(output_pipe) == -1) { + throw std::runtime_error("Failed to create pipes"); + } + + pid_t pid = fork(); + if (pid == -1) { + throw std::runtime_error("Failed to fork"); + } + + if (pid == 0) { // Child + dup2(input_pipe[0], STDIN_FILENO); + dup2(output_pipe[1], STDOUT_FILENO); + close(input_pipe[0]); + close(input_pipe[1]); + close(output_pipe[0]); + close(output_pipe[1]); + + std::vector argv; + for (const auto& arg : args) argv.push_back(const_cast(arg.c_str())); + argv.push_back(nullptr); + + execvp(argv[0], argv.data()); + exit(1); + } + + // Parent + close(input_pipe[0]); + close(output_pipe[1]); + + if (!input.empty()) { + write(input_pipe[1], input.c_str(), input.size()); + } + close(input_pipe[1]); + + std::stringstream output; + char buffer[4096]; + ssize_t n; + while ((n = read(output_pipe[0], buffer, sizeof(buffer))) > 0) { + output.write(buffer, n); + } + close(output_pipe[0]); + + int status; + waitpid(pid, &status, 0); + return output.str(); +} + +void cron_manager::register_job(const std::string& path, const std::string& schedule, const std::string& title) { + char current_path[4096]; + if (readlink("/proc/self/exe", current_path, sizeof(current_path)) == -1) { + throw std::runtime_error("Failed to get current executable path"); + } + std::string alloy_exe = std::string(current_path); + + std::string current_crontab = run_command_with_args({"crontab", "-l"}); + std::stringstream ss(current_crontab); + std::string line; + std::vector lines; + std::string marker = "# Alloy-cron: " + title; + bool in_old_job = false; + + while (std::getline(ss, line)) { + if (line == marker) { + in_old_job = true; + continue; + } + if (in_old_job) { + in_old_job = false; + continue; + } + lines.push_back(line); + } + + std::stringstream new_crontab; + for (const auto& l : lines) { + new_crontab << l << "\n"; + } + + new_crontab << marker << "\n"; + new_crontab << schedule << " '" << alloy_exe << "' run --cron-title='" << title << "' --cron-period='" << schedule << "' '" << path << "'\n"; + + run_command_with_args({"crontab", "-"}, new_crontab.str()); +} + +void cron_manager::remove_job(const std::string& title) { + std::string current_crontab = run_command_with_args({"crontab", "-l"}); + std::stringstream ss(current_crontab); + std::string line; + std::vector lines; + std::string marker = "# Alloy-cron: " + title; + bool in_old_job = false; + bool found = false; + + while (std::getline(ss, line)) { + if (line == marker) { + in_old_job = true; + found = true; + continue; + } + if (in_old_job) { + in_old_job = false; + continue; + } + lines.push_back(line); + } + + if (found) { + std::stringstream new_crontab; + for (const auto& l : lines) { + new_crontab << l << "\n"; + } + run_command_with_args({"crontab", "-"}, new_crontab.str()); + } +} + +} // namespace alloy diff --git a/alloy/cron_manager_macos.mm b/alloy/cron_manager_macos.mm new file mode 100644 index 000000000..6e1755708 --- /dev/null +++ b/alloy/cron_manager_macos.mm @@ -0,0 +1,118 @@ +#include "cron_manager.hpp" +#include "cron_parser.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace alloy { + +void cron_manager::register_job(const std::string& path, const std::string& schedule, const std::string& title) { + auto expr = cron_parser::parse(schedule); + + char current_path[4096]; + uint32_t size = sizeof(current_path); + if (_NSGetExecutablePath(current_path, &size) != 0) { + throw std::runtime_error("Failed to get current executable path"); + } + std::string alloy_exe = std::string(current_path); + + struct passwd *pw = getpwuid(getuid()); + std::string home_dir = pw->pw_dir; + std::string plist_path = home_dir + "/Library/LaunchAgents/Alloy.cron." + title + ".plist"; + + std::stringstream ss; + ss << "\n"; + ss << "\n"; + ss << "\n"; + ss << "\n"; + ss << " Label\n"; + ss << " Alloy.cron." << title << "\n"; + ss << " ProgramArguments\n"; + ss << " \n"; + ss << " " << alloy_exe << "\n"; + ss << " run\n"; + ss << " --cron-title=" << title << "\n"; + ss << " --cron-period=" << schedule << "\n"; + ss << " " << path << "\n"; + ss << " \n"; + ss << " StartCalendarInterval\n"; + ss << " \n"; + + auto generate_intervals = [&](const std::set& months, const std::set& days, const std::set& hours, const std::set& minutes, const std::set& weekdays) { + for (int m : months) { + for (int d : days) { + for (int h : hours) { + for (int mi : minutes) { + for (int w : weekdays) { + ss << " \n"; + if (m != -1) ss << " Month" << m << "\n"; + if (d != -1) ss << " Day" << d << "\n"; + if (h != -1) ss << " Hour" << h << "\n"; + if (mi != -1) ss << " Minute" << mi << "\n"; + if (w != -1) ss << " Weekday" << w << "\n"; + ss << " \n"; + } + } + } + } + } + }; + + std::set m_set = expr.months.size() == 12 ? std::set{-1} : expr.months; + std::set d_set = expr.days_of_month.size() == 31 ? std::set{-1} : expr.days_of_month; + std::set h_set = expr.hours.size() == 24 ? std::set{-1} : expr.hours; + std::set mi_set = expr.minutes.size() == 60 ? std::set{-1} : expr.minutes; + std::set w_set = expr.days_of_week.size() == 7 ? std::set{-1} : expr.days_of_week; + + if (expr.dom_restricted && expr.dow_restricted) { + generate_intervals(m_set, d_set, h_set, mi_set, std::set{-1}); + generate_intervals(m_set, std::set{-1}, h_set, mi_set, w_set); + } else if (expr.dom_restricted) { + generate_intervals(m_set, d_set, h_set, mi_set, std::set{-1}); + } else if (expr.dow_restricted) { + generate_intervals(m_set, std::set{-1}, h_set, mi_set, w_set); + } else { + generate_intervals(m_set, std::set{-1}, h_set, mi_set, std::set{-1}); + } + + ss << " \n"; + ss << " StandardOutPath\n"; + ss << " /tmp/Alloy.cron." << title << ".stdout.log\n"; + ss << " StandardErrorPath\n"; + ss << " /tmp/Alloy.cron." << title << ".stderr.log\n"; + ss << "\n"; + ss << "\n"; + + std::ofstream ofs(plist_path); + ofs << ss.str(); + ofs.close(); + + pid_t pid = fork(); + if (pid == 0) { + execlp("launchctl", "launchctl", "load", plist_path.c_str(), nullptr); + exit(1); + } + waitpid(pid, nullptr, 0); +} + +void cron_manager::remove_job(const std::string& title) { + struct passwd *pw = getpwuid(getuid()); + std::string home_dir = pw->pw_dir; + std::string plist_path = home_dir + "/Library/LaunchAgents/Alloy.cron." + title + ".plist"; + + pid_t pid = fork(); + if (pid == 0) { + execlp("launchctl", "launchctl", "unload", plist_path.c_str(), nullptr); + exit(1); + } + waitpid(pid, nullptr, 0); + unlink(plist_path.c_str()); +} + +} // namespace alloy diff --git a/alloy/cron_manager_windows.cpp b/alloy/cron_manager_windows.cpp new file mode 100644 index 000000000..5b8931d68 --- /dev/null +++ b/alloy/cron_manager_windows.cpp @@ -0,0 +1,130 @@ +#include "cron_manager.hpp" +#include "cron_parser.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace alloy { + +void cron_manager::register_job(const std::string& path, const std::string& schedule, const std::string& title) { + auto expr = cron_parser::parse(schedule); + + char current_path[4096]; + if (getcwd(current_path, sizeof(current_path)) == nullptr) { + throw std::runtime_error("Failed to get current directory"); + } + std::string alloy_exe = std::string(current_path) + "\\alloy_bin.exe"; + + std::stringstream ss; + ss << "\n"; + ss << "\n"; + ss << " \n"; + + auto generate_triggers = [&](const std::set& months, const std::set& days, const std::set& hours, const std::set& minutes, const std::set& weekdays) { + int count = 0; + // Check for Repetition (Step) compatibility + bool minute_repetition = false; + int interval = 0; + if (minutes.size() > 1) { + std::vector sorted_minutes(minutes.begin(), minutes.end()); + int diff = sorted_minutes[1] - sorted_minutes[0]; + bool uniform = true; + for (size_t i = 1; i < sorted_minutes.size(); ++i) { + if (sorted_minutes[i] - sorted_minutes[i-1] != diff) { uniform = false; break; } + } + if (uniform && 60 % diff == 0) { + minute_repetition = true; + interval = diff; + } + } + + // Simplistic trigger generation for demonstration/implementation + if (minute_repetition && hours.size() == 24 && days.size() == 31 && months.size() == 12 && weekdays.size() == 7) { + ss << " \n"; + ss << " 2025-01-01T00:00:00\n"; + ss << " \n"; + ss << " PT" << interval << "M\n"; + ss << " \n"; + ss << " 1\n"; + ss << " \n"; + return 1; + } + + // Expand individual triggers + for (int m : months) { + for (int d : days) { + for (int h : hours) { + for (int mi : (minute_repetition ? std::set{minutes.begin(), minutes.begin()} : minutes)) { + ss << " \n"; + ss << " 2025-" << (m < 10 ? "0" : "") << m << "-" << (d < 10 ? "0" : "") << d << "T" << (h < 10 ? "0" : "") << h << ":" << (mi < 10 ? "0" : "") << mi << ":00\n"; + if (minute_repetition) { + ss << " PT" << interval << "M\n"; + } + ss << " \n"; + ss << " " << d << "\n"; + ss << " " << m << "\n"; + ss << " \n"; + ss << " \n"; + count++; + } + } + } + } + return count; + }; + + int trigger_count = generate_triggers(expr.months, expr.days_of_month, expr.hours, expr.minutes, expr.days_of_week); + + if (trigger_count > 48) { + throw std::runtime_error("Windows Task Scheduler limit exceeded: max 48 triggers per task"); + } + + ss << " \n"; + ss << " \n"; + ss << " \n"; + ss << " \"" << alloy_exe << "\"\n"; + ss << " run --cron-title=\"" << title << "\" --cron-period=\"" << schedule << "\" \"" << path << "\"\n"; + ss << " \n"; + ss << " \n"; + ss << " S4U\n"; + ss << "\n"; + + std::string xml_path = "Alloy-cron-" + title + ".xml"; + std::ofstream ofs(xml_path); + ofs << ss.str(); + ofs.close(); + + std::string cmd_line = "schtasks /create /xml \"" + xml_path + "\" /tn \"Alloy-cron-" + title + "\" /f"; + + STARTUPINFO si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + if (CreateProcess(NULL, (LPSTR)cmd_line.c_str(), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + DeleteFile(xml_path.c_str()); +} + +void cron_manager::remove_job(const std::string& title) { + std::string task_name = "Alloy-cron-" + title; + std::string cmd_line = "schtasks /delete /tn \"" + task_name + "\" /f"; + + STARTUPINFO si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + if (CreateProcess(NULL, (LPSTR)cmd_line.c_str(), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } +} + +} // namespace alloy diff --git a/alloy/cron_parser.cpp b/alloy/cron_parser.cpp new file mode 100644 index 000000000..cf2ee2a4f --- /dev/null +++ b/alloy/cron_parser.cpp @@ -0,0 +1,174 @@ +#include "cron_parser.hpp" +#include +#include +#include +#include +#include +#include + +namespace alloy { + +#ifdef _WIN32 +#define gmtime_r(t, tm) gmtime_s(tm, t) +static time_t timegm(struct tm* tm) { return _mkgmtime(tm); } +#endif + +static const std::vector MONTH_NAMES = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; +static const std::vector DAY_NAMES = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + +std::string cron_parser::normalize_expression(const std::string& expression) { + if (expression == "@yearly" || expression == "@annually") return "0 0 1 1 *"; + if (expression == "@monthly") return "0 0 1 * *"; + if (expression == "@weekly") return "0 0 * * 0"; + if (expression == "@daily" || expression == "@midnight") return "0 0 * * *"; + if (expression == "@hourly") return "0 * * * *"; + return expression; +} + +cron_parser::cron_expression cron_parser::parse(const std::string& expression) { + std::string norm_expr = normalize_expression(expression); + std::istringstream iss(norm_expr); + std::vector fields; + std::string field; + while (iss >> field) { + fields.push_back(field); + } + + if (fields.size() != 5) { + throw std::runtime_error("Invalid cron expression: expected 5 fields"); + } + + cron_expression expr; + expr.original_expression = expression; + expr.minutes = parse_field(fields[0], 0, 59); + expr.hours = parse_field(fields[1], 0, 23); + expr.days_of_month = parse_field(fields[2], 1, 31); + expr.months = parse_field(fields[3], 1, 12, MONTH_NAMES); + expr.days_of_week = parse_field(fields[4], 0, 7, DAY_NAMES); + + if (expr.days_of_week.count(7)) { + expr.days_of_week.insert(0); + expr.days_of_week.erase(7); + } + + expr.dom_restricted = (fields[2] != "*"); + expr.dow_restricted = (fields[4] != "*"); + + return expr; +} + +std::set cron_parser::parse_field(const std::string& field, int min_val, int max_val, const std::vector& names) { + std::set values; + std::string normalized_field = field; + std::transform(normalized_field.begin(), normalized_field.end(), normalized_field.begin(), ::toupper); + + auto parse_val = [&](const std::string& s) -> int { + if (std::all_of(s.begin(), s.end(), ::isdigit)) { + return std::stoi(s); + } + for (size_t i = 0; i < names.size(); ++i) { + if (s.find(names[i]) == 0) { + return static_cast(i) + (min_val == 1 ? 1 : 0); + } + } + throw std::runtime_error("Invalid value in cron field: " + s); + }; + + std::istringstream iss(normalized_field); + std::string part; + while (std::getline(iss, part, ',')) { + size_t slash_pos = part.find('/'); + int step = 1; + std::string range_part = part; + if (slash_pos != std::string::npos) { + step = std::stoi(part.substr(slash_pos + 1)); + range_part = part.substr(0, slash_pos); + } + + int start = min_val, end = max_val; + if (range_part != "*") { + size_t dash_pos = range_part.find('-'); + if (dash_pos != std::string::npos) { + start = parse_val(range_part.substr(0, dash_pos)); + end = parse_val(range_part.substr(dash_pos + 1)); + } else { + start = end = parse_val(range_part); + } + } + + for (int i = start; i <= end; i += step) { + if (i >= min_val && i <= max_val) { + values.insert(i); + } + } + } + return values; +} + +std::chrono::system_clock::time_point cron_parser::next(const cron_expression& expr, std::chrono::system_clock::time_point relative_to) { + std::time_t t = std::chrono::system_clock::to_time_t(relative_to); + std::tm tm_buf; + gmtime_r(&t, &tm_buf); + + tm_buf.tm_sec = 0; + tm_buf.tm_min++; + + for (int i = 0; i < 4 * 366; ++i) { + std::time_t current_t = timegm(&tm_buf); + gmtime_r(¤t_t, &tm_buf); + + if (expr.months.count(tm_buf.tm_mon + 1) == 0) { + auto it = expr.months.lower_bound(tm_buf.tm_mon + 2); + if (it == expr.months.end()) { + tm_buf.tm_year++; + tm_buf.tm_mon = *expr.months.begin() - 1; + } else { + tm_buf.tm_mon = *it - 1; + } + tm_buf.tm_mday = 1; + tm_buf.tm_hour = 0; + tm_buf.tm_min = 0; + continue; + } + + bool dom_match = expr.days_of_month.count(tm_buf.tm_mday) > 0; + bool dow_match = expr.days_of_week.count(tm_buf.tm_wday) > 0; + bool day_match = (expr.dom_restricted && expr.dow_restricted) ? (dom_match || dow_match) : (expr.dom_restricted ? dom_match : (expr.dow_restricted ? dow_match : true)); + + if (!day_match) { + tm_buf.tm_mday++; + tm_buf.tm_hour = 0; + tm_buf.tm_min = 0; + continue; + } + + if (expr.hours.count(tm_buf.tm_hour) == 0) { + auto it = expr.hours.lower_bound(tm_buf.tm_hour + 1); + if (it == expr.hours.end()) { + tm_buf.tm_mday++; + tm_buf.tm_hour = *expr.hours.begin(); + } else { + tm_buf.tm_hour = *it; + } + tm_buf.tm_min = 0; + continue; + } + + if (expr.minutes.count(tm_buf.tm_min) == 0) { + auto it = expr.minutes.lower_bound(tm_buf.tm_min + 1); + if (it == expr.minutes.end()) { + tm_buf.tm_hour++; + tm_buf.tm_min = *expr.minutes.begin(); + } else { + tm_buf.tm_min = *it; + } + continue; + } + + return std::chrono::system_clock::from_time_t(timegm(&tm_buf)); + } + + throw std::runtime_error("No matching time found within 4 years"); +} + +} // namespace alloy diff --git a/alloy/cron_parser.hpp b/alloy/cron_parser.hpp new file mode 100644 index 000000000..56e37f4cc --- /dev/null +++ b/alloy/cron_parser.hpp @@ -0,0 +1,34 @@ +#ifndef ALLOY_CRON_PARSER_HPP +#define ALLOY_CRON_PARSER_HPP + +#include +#include +#include +#include + +namespace alloy { + +class cron_parser { +public: + struct cron_expression { + std::set minutes; + std::set hours; + std::set days_of_month; + std::set months; + std::set days_of_week; + bool dom_restricted = false; + bool dow_restricted = false; + std::string original_expression; + }; + + static cron_expression parse(const std::string& expression); + static std::chrono::system_clock::time_point next(const cron_expression& expr, std::chrono::system_clock::time_point relative_to); + +private: + static std::set parse_field(const std::string& field, int min_val, int max_val, const std::vector& names = {}); + static std::string normalize_expression(const std::string& expression); +}; + +} // namespace alloy + +#endif // ALLOY_CRON_PARSER_HPP diff --git a/alloy/executor_test.cpp b/alloy/executor_test.cpp new file mode 100644 index 000000000..aefa8547e --- /dev/null +++ b/alloy/executor_test.cpp @@ -0,0 +1,35 @@ +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) { + if (argc > 1 && std::string(argv[1]) == "run") { + std::string cron_title, cron_period, script_path; + for (int i = 2; i < argc; ++i) { + std::string arg = argv[i]; + if (arg.find("--cron-title=") == 0) { + cron_title = arg.substr(13); + } else if (arg.find("--cron-period=") == 0) { + cron_period = arg.substr(14); + } else if (arg[0] != '-') { + script_path = arg; + } + } + + std::cout << "Executing cron job: " << cron_title << " (" << cron_period << ") with script: " << script_path << std::endl; + + char current_path[4096]; + if (getcwd(current_path, sizeof(current_path)) == nullptr) { + return 1; + } + std::string wrapper_path = std::string(current_path) + "/alloy/executor_wrapper.js"; + + std::string cmd = "node \"" + wrapper_path + "\" \"" + cron_title + "\" \"" + cron_period + "\" \"" + script_path + "\""; + int ret = system(cmd.c_str()); + return WEXITSTATUS(ret); + } + return 0; +} diff --git a/alloy/executor_wrapper.js b/alloy/executor_wrapper.js new file mode 100644 index 000000000..dbd7f6968 --- /dev/null +++ b/alloy/executor_wrapper.js @@ -0,0 +1,28 @@ +const fs = require('fs'); +const path = require('path'); + +const args = process.argv.slice(2); +const cronTitle = args[0]; +const cronPeriod = args[1]; +const scriptPath = args[2]; + +const fullPath = path.resolve(scriptPath); +const worker = require(fullPath); + +if (worker.default && typeof worker.default.scheduled === 'function') { + const controller = { + cron: cronPeriod, + type: "scheduled", + scheduledTime: Date.now() + }; + + Promise.resolve(worker.default.scheduled(controller)) + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); +} else { + console.error("Worker does not export a default object with a scheduled() method"); + process.exit(1); +} diff --git a/alloy/main.cpp b/alloy/main.cpp new file mode 100644 index 000000000..b2f449eed --- /dev/null +++ b/alloy/main.cpp @@ -0,0 +1,86 @@ +#include "webview/webview.h" +#include "runtime.hpp" +#include +#include +#include + +#ifdef _WIN32 +#include +#include + +int WINAPI WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/, + LPSTR lpCmdLine, int /*nCmdShow*/) { + int argc; + LPWSTR* argv_w = CommandLineToArgvW(GetCommandLineW(), &argc); + std::vector args; + for (int i = 0; i < argc; ++i) { + std::wstring warg(argv_w[i]); + args.push_back(std::string(warg.begin(), warg.end())); + } + LocalFree(argv_w); +#else +int main(int argc, char** argv) { + std::vector args; + for (int i = 0; i < argc; ++i) args.push_back(argv[i]); +#endif + + if (args.size() > 1 && args[1] == "run") { + std::string cron_title, cron_period, script_path; + for (size_t i = 2; i < args.size(); ++i) { + std::string arg = args[i]; + if (arg.find("--cron-title=") == 0) { + cron_title = arg.substr(13); + } else if (arg.find("--cron-period=") == 0) { + cron_period = arg.substr(14); + } else if (arg[0] != '-') { + script_path = arg; + } + } + + std::cout << "Executing cron job: " << cron_title << " (" << cron_period << ") with script: " << script_path << std::endl; + + char current_path[4096]; +#ifdef _WIN32 + GetModuleFileName(NULL, current_path, sizeof(current_path)); + std::string alloy_exe = std::string(current_path); + std::string dir = alloy_exe.substr(0, alloy_exe.find_last_of("\\/")); + std::string wrapper_path = dir + "\\executor_wrapper.js"; +#elif defined(__APPLE__) + uint32_t size = sizeof(current_path); + _NSGetExecutablePath(current_path, &size); + std::string alloy_exe = std::string(current_path); + std::string dir = alloy_exe.substr(0, alloy_exe.find_last_of("/")); + std::string wrapper_path = dir + "/executor_wrapper.js"; +#else + readlink("/proc/self/exe", current_path, sizeof(current_path)); + std::string alloy_exe = std::string(current_path); + std::string dir = alloy_exe.substr(0, alloy_exe.find_last_of("/")); + std::string wrapper_path = dir + "/executor_wrapper.js"; +#endif + + std::vector node_cmd = {"node", wrapper_path, cron_title, cron_period, script_path}; + alloy::spawn_options options; + options.stdout_mode = "inherit"; + options.stderr_mode = "inherit"; + + alloy::subprocess proc(node_cmd, options); + return proc.wait(); + } + + try { + webview::webview w(true, nullptr); + w.set_title("AlloyScript Runtime"); + w.set_size(800, 600, WEBVIEW_HINT_NONE); + + alloy::runtime::init(w); + + w.set_html("

AlloyScript Runtime

Open devtools to interact with window.Alloy.cron

"); + w.run(); + alloy::runtime::stop(); + } catch (const webview::exception &e) { + std::cerr << e.what() << '\n'; + return 1; + } + + return 0; +} diff --git a/alloy/process_manager.cpp b/alloy/process_manager.cpp new file mode 100644 index 000000000..5e1d5eade --- /dev/null +++ b/alloy/process_manager.cpp @@ -0,0 +1,34 @@ +#include "process_manager.hpp" + +namespace alloy { + +process_manager& process_manager::instance() { + static process_manager manager; + return manager; +} + +std::string process_manager::register_proc(std::shared_ptr proc) { + std::lock_guard lock(m_mutex); + std::string id = std::to_string(m_next_id++); + m_procs[id] = proc; + return id; +} + +std::shared_ptr process_manager::get_proc(const std::string& id) { + std::lock_guard lock(m_mutex); + auto it = m_procs.find(id); + if (it != m_procs.end()) return it->second; + return nullptr; +} + +void process_manager::unregister_proc(const std::string& id) { + std::lock_guard lock(m_mutex); + m_procs.erase(id); +} + +std::map> process_manager::get_all_procs() { + std::lock_guard lock(m_mutex); + return m_procs; +} + +} // namespace alloy diff --git a/alloy/process_manager.hpp b/alloy/process_manager.hpp new file mode 100644 index 000000000..24b464d90 --- /dev/null +++ b/alloy/process_manager.hpp @@ -0,0 +1,31 @@ +#ifndef ALLOY_PROCESS_MANAGER_HPP +#define ALLOY_PROCESS_MANAGER_HPP + +#include "subprocess.hpp" +#include +#include +#include +#include + +namespace alloy { + +class process_manager { +public: + static process_manager& instance(); + + std::string register_proc(std::shared_ptr proc); + std::shared_ptr get_proc(const std::string& id); + void unregister_proc(const std::string& id); + + std::map> get_all_procs(); + +private: + process_manager() = default; + std::mutex m_mutex; + std::map> m_procs; + int m_next_id = 1; +}; + +} // namespace alloy + +#endif // ALLOY_PROCESS_MANAGER_HPP diff --git a/alloy/runtime.cpp b/alloy/runtime.cpp new file mode 100644 index 000000000..1239d2cbc --- /dev/null +++ b/alloy/runtime.cpp @@ -0,0 +1,292 @@ +#include "runtime.hpp" +#include "cron_parser.hpp" +#include "cron_manager.hpp" +#include "subprocess.hpp" +#include "process_manager.hpp" +#include "shell_lexer.hpp" +#include "shell_parser.hpp" +#include "shell_interpreter.hpp" +#include "webview/json_deprecated.hh" +#include +#include +#include +#include +#include + +namespace alloy { + +static std::atomic g_runtime_running{true}; + +void runtime::stop() { + g_runtime_running = false; +} + +void runtime::init(webview::webview& w) { + w.bind("Alloy_cron_parse", [&](const std::string& req) -> std::string { + try { + std::string expression = webview::json_parse(req, "", 0); + std::string relative_date_str = webview::json_parse(req, "", 1); + if (expression.empty()) return "null"; + auto expr = cron_parser::parse(expression); + auto relative_to = std::chrono::system_clock::now(); + if (!relative_date_str.empty()) { + try { + auto ms = std::stoll(relative_date_str); + relative_to = std::chrono::system_clock::time_point(std::chrono::milliseconds(ms)); + } catch (...) {} + } + auto next_time = cron_parser::next(expr, relative_to); + std::time_t t = std::chrono::system_clock::to_time_t(next_time); + std::tm tm_buf; +#ifdef _WIN32 + gmtime_s(&tm_buf, &t); +#else + gmtime_r(&t, &tm_buf); +#endif + std::ostringstream oss; + oss << "\"" << std::put_time(&tm_buf, "%Y-%m-%dT%H:%M:%S.000Z") << "\""; + return oss.str(); + } catch (...) { return "null"; } + }); + + w.bind("Alloy_cron_register", [&](const std::string& req) -> std::string { + try { + std::string path = webview::json_parse(req, "", 0); + std::string schedule = webview::json_parse(req, "", 1); + std::string title = webview::json_parse(req, "", 2); + if (path.empty() || schedule.empty() || title.empty()) return "false"; + cron_manager::register_job(path, schedule, title); + return "true"; + } catch (...) { return "false"; } + }); + + w.bind("Alloy_cron_remove", [&](const std::string& req) -> std::string { + try { + std::string title = webview::json_parse(req, "", 0); + if (title.empty()) return "false"; + cron_manager::remove_job(title); + return "true"; + } catch (...) { return "false"; } + }); + + w.bind("Alloy_spawn", [&](const std::string& req) -> std::string { + try { + std::string cmd_json = webview::json_parse(req, "", 0); + std::string options_json = webview::json_parse(req, "", 1); + std::vector cmd; + for(int i=0; ; ++i) { + std::string arg = webview::json_parse(cmd_json, "", i); + if (arg.empty()) break; + cmd.push_back(arg); + } + spawn_options options; + options.cwd = webview::json_parse(options_json, "cwd", -1); + auto proc = std::make_shared(cmd, options); + std::string id = process_manager::instance().register_proc(proc); + std::ostringstream oss; + oss << "{\"id\":\"" << id << "\", \"pid\":" << proc->pid() << "}"; + return oss.str(); + } catch (...) { return "null"; } + }); + + w.bind("Alloy_spawnSync", [&](const std::string& req) -> std::string { + try { + std::string cmd_json = webview::json_parse(req, "", 0); + std::vector cmd; + for(int i=0; ; ++i) { + std::string arg = webview::json_parse(cmd_json, "", i); + if (arg.empty()) break; + cmd.push_back(arg); + } + spawn_options options; + auto proc = std::make_shared(cmd, options); + auto res = proc->wait_sync(); + std::ostringstream oss; + oss << "{\"exitCode\":" << res.exit_code + << ",\"stdout\":\"" << webview::detail::json_escape(res.stdout_data) << "\"" + << ",\"stderr\":\"" << webview::detail::json_escape(res.stderr_data) << "\"}"; + return oss.str(); + } catch (...) { return "null"; } + }); + + w.bind("Alloy_proc_kill", [&](const std::string& req) -> std::string { + std::string id = webview::json_parse(req, "", 0); + auto proc = process_manager::instance().get_proc(id); + if (proc) { proc->kill(); return "true"; } + return "false"; + }); + + w.bind("Alloy_shell_exec", [&](const std::string& req) -> std::string { + try { + std::string cmd_str = webview::json_parse(req, "", 0); + std::string placeholders_json = webview::json_parse(req, "", 1); + std::string options_json = webview::json_parse(req, "", 2); + + std::vector placeholders; + for(int i=0; ; ++i) { + std::string p = webview::json_parse(placeholders_json, "", i); + if (p.empty()) break; + placeholders.push_back(p); + } + + auto tokens = shell_lexer::tokenize(cmd_str); + auto pipeline = shell_parser::parse(tokens); + + std::map env; + std::string cwd = webview::json_parse(options_json, "cwd", -1); + + auto res = shell_interpreter::execute(pipeline, env, cwd, placeholders); + + std::ostringstream oss; + oss << "{\"exitCode\":" << res.exit_code + << ",\"stdout\":\"" << webview::detail::json_escape(res.stdout_data) << "\"" + << ",\"stderr\":\"" << webview::detail::json_escape(res.stderr_data) << "\"}"; + return oss.str(); + } catch (...) { return "null"; } + }); + + w.bind("Alloy_proc_stdin_write", [&](const std::string& req) -> std::string { + std::string id = webview::json_parse(req, "", 0); + std::string data = webview::json_parse(req, "", 1); + auto proc = process_manager::instance().get_proc(id); + if (proc) { proc->stdin_write(data); return "true"; } + return "false"; + }); + + std::thread([&w]() { + while (g_runtime_running) { + auto procs = process_manager::instance().get_all_procs(); + if (procs.empty()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } + + for (auto& pair : procs) { + auto id = pair.first; + auto proc = pair.second; + + auto check_stream = [&](pipe_handle_t fd, const std::string& stream_name) { + if (fd == -1) return; + char buf[4096]; + // Non-blocking check would be better, but for simplicity we use read_pipe + // with a mechanism that doesn't block forever if possible. + // On POSIX, we could use fcntl O_NONBLOCK. +#ifndef _WIN32 + int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); +#endif + ssize_t n = subprocess::read_pipe(fd, buf, sizeof(buf)); + if (n > 0) { + std::string data(buf, n); + std::string js = "window.Alloy._onProcData('" + id + "', '" + stream_name + "', " + webview::detail::json_escape(data) + ")"; + w.dispatch([&w, js]() { w.eval(js); }); + } + }; + + check_stream(proc->get_stdout_fd(), "stdout"); + check_stream(proc->get_stderr_fd(), "stderr"); + + if (!proc->is_alive()) { + int code = proc->wait(); + std::string js = "window.Alloy._onProcExit('" + id + "', " + std::to_string(code) + ")"; + w.dispatch([&w, js]() { w.eval(js); }); + process_manager::instance().unregister_proc(id); + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + }).detach(); + + w.init(R"js( + window.Alloy = window.Alloy || {}; + window.Alloy.cron = function(path, schedule, title) { return window.Alloy_cron_register(path, schedule, title); }; + window.Alloy.cron.parse = function(expression, relativeDate) { + const result = window.Alloy_cron_parse(expression, relativeDate ? relativeDate.getTime() : null); + return result ? new Date(result) : null; + }; + window.Alloy.cron.remove = function(title) { return window.Alloy_cron_remove(title); }; + window.Alloy._procs = {}; + window.Alloy._onProcData = function(id, stream, data) { if (window.Alloy._procs[id]) window.Alloy._procs[id]._onData(stream, data); }; + window.Alloy._onProcExit = function(id, code) { if (window.Alloy._procs[id]) window.Alloy._procs[id]._onExit(code); delete window.Alloy._procs[id]; }; + window.Alloy.spawn = function(cmd, options) { + const res = JSON.parse(window.Alloy_spawn(cmd, options)); + if (!res) return null; + const proc = new Alloy.Subprocess(res.id, res.pid); + window.Alloy._procs[res.id] = proc; + return proc; + }; + window.Alloy.spawnSync = function(cmd, options) { return JSON.parse(window.Alloy_spawnSync(cmd, options)); }; + window.Alloy.$ = function(strings, ...values) { + let cmd = strings[0]; + const placeholders = []; + for (let i = 0; i < values.length; i++) { + placeholders.push(String(values[i])); + cmd += "${" + i + "}" + strings[i + 1]; + } + return new window.Alloy.ShellPromise(cmd, placeholders); + }; + + window.Alloy.ShellPromise = class { + constructor(cmd, placeholders) { + this._cmd = cmd; + this._placeholders = placeholders; + this._options = { cwd: "", env: {} }; + this._quiet = false; + this._nothrow = false; + } + quiet() { this._quiet = true; return this; } + nothrow() { this._nothrow = true; return this; } + cwd(path) { this._options.cwd = path; return this; } + env(env) { this._options.env = env; return this; } + async text() { + this.quiet(); + const res = await this._exec(); + return res.stdout; + } + async json() { + const text = await this.text(); + return JSON.parse(text); + } + then(resolve, reject) { + return this._exec().then(resolve, reject); + } + async _exec() { + const res = JSON.parse(window.Alloy_shell_exec(this._cmd, this._placeholders, this._options)); + if (!this._quiet) { + if (res.stdout) console.log(res.stdout); + if (res.stderr) console.error(res.stderr); + } + if (!this._nothrow && res.exitCode !== 0) { + throw new Error(`Shell command failed with code ${res.exitCode}`); + } + return res; + } + }; + + window.Alloy.Subprocess = class { + constructor(id, pid) { + this.id = id; this.pid = pid; + this._handlers = { stdout: [], stderr: [] }; + this.exited = new Promise((resolve) => { this._resolveExited = resolve; }); + } + _onData(stream, data) { this._handlers[stream].forEach(h => h(data)); } + _onExit(code) { this._resolveExited(code); } + kill(signal) { window.Alloy_proc_kill(this.id, signal); } + unref() {} + get stdout() { + return { + text: () => new Promise(resolve => { + let out = ""; const h = (d) => { out += d; }; + this._handlers.stdout.push(h); + this.exited.then(() => resolve(out)); + }), + on: (event, handler) => { if(event==='data') this._handlers.stdout.push(handler); } + }; + } + get stderr() { + return { + on: (event, handler) => { if(event==='data') this._handlers.stderr.push(handler); } + }; + } + }; + )js"); +} + +} // namespace alloy diff --git a/alloy/runtime.hpp b/alloy/runtime.hpp new file mode 100644 index 000000000..8ebc94072 --- /dev/null +++ b/alloy/runtime.hpp @@ -0,0 +1,17 @@ +#ifndef ALLOY_RUNTIME_HPP +#define ALLOY_RUNTIME_HPP + +#include "webview/webview.h" +#include + +namespace alloy { + +class runtime { +public: + static void init(webview::webview& w); + static void stop(); +}; + +} // namespace alloy + +#endif // ALLOY_RUNTIME_HPP diff --git a/alloy/shell_builtins.cpp b/alloy/shell_builtins.cpp new file mode 100644 index 000000000..9bbc4f893 --- /dev/null +++ b/alloy/shell_builtins.cpp @@ -0,0 +1,125 @@ +#include "shell_builtins.hpp" +#include +#include +#include +#include + +namespace alloy { + +namespace fs = std::filesystem; + +bool shell_builtins::is_builtin(const std::string& cmd) { + static const std::vector builtins = { + "cd", "ls", "rm", "echo", "pwd", "mkdir", "touch", "cat", "which", "mv", "exit", "true", "false", "yes", "seq", "dirname", "basename" + }; + return std::find(builtins.begin(), builtins.end(), cmd) != builtins.end(); +} + +int shell_builtins::execute(const std::string& cmd, const std::vector& args, std::istream& in, std::ostream& out, std::ostream& err) { + if (cmd == "cd") { + if (args.empty()) return 0; + try { + fs::current_path(args[0]); + return 0; + } catch (const std::exception& e) { + err << "cd: " << e.what() << std::endl; + return 1; + } + } else if (cmd == "pwd") { + out << fs::current_path().string() << std::endl; + return 0; + } else if (cmd == "ls") { + std::string path = args.empty() ? "." : args[0]; + try { + for (const auto& entry : fs::directory_iterator(path)) { + out << entry.path().filename().string() << std::endl; + } + return 0; + } catch (const std::exception& e) { + err << "ls: " << e.what() << std::endl; + return 1; + } + } else if (cmd == "mkdir") { + if (args.empty()) return 1; + try { + fs::create_directories(args[0]); + return 0; + } catch (const std::exception& e) { + err << "mkdir: " << e.what() << std::endl; + return 1; + } + } else if (cmd == "rm") { + if (args.empty()) return 1; + try { + for (const auto& arg : args) { + fs::remove_all(arg); + } + return 0; + } catch (const std::exception& e) { + err << "rm: " << e.what() << std::endl; + return 1; + } + } else if (cmd == "touch") { + if (args.empty()) return 1; + try { + for (const auto& arg : args) { + std::ofstream ofs(arg, std::ios::app); + } + return 0; + } catch (const std::exception& e) { + err << "touch: " << e.what() << std::endl; + return 1; + } + } else if (cmd == "mv") { + if (args.size() < 2) return 1; + try { + fs::rename(args[0], args[1]); + return 0; + } catch (const std::exception& e) { + err << "mv: " << e.what() << std::endl; + return 1; + } + } else if (cmd == "echo") { + for (size_t i = 0; i < args.size(); ++i) { + out << args[i] << (i + 1 < args.size() ? " " : ""); + } + out << std::endl; + return 0; + } else if (cmd == "cat") { + if (args.empty()) { + out << in.rdbuf(); + return 0; + } + for (const auto& arg : args) { + std::ifstream ifs(arg); + if (ifs) out << ifs.rdbuf(); + else { err << "cat: " << arg << ": No such file or directory" << std::endl; return 1; } + } + return 0; + } else if (cmd == "dirname") { + if (args.empty()) return 1; + out << fs::path(args[0]).parent_path().string() << std::endl; + return 0; + } else if (cmd == "basename") { + if (args.empty()) return 1; + out << fs::path(args[0]).filename().string() << std::endl; + return 0; + } else if (cmd == "true") { + return 0; + } else if (cmd == "false") { + return 1; + } else if (cmd == "exit") { + exit(args.empty() ? 0 : std::stoi(args[0])); + } else if (cmd == "yes") { + std::string s = args.empty() ? "y" : args[0]; + while (true) out << s << std::endl; + } else if (cmd == "seq") { + if (args.empty()) return 1; + int end = std::stoi(args[0]); + for (int i = 1; i <= end; ++i) out << i << std::endl; + return 0; + } + return -1; // Not handled here +} + +} // namespace alloy diff --git a/alloy/shell_builtins.hpp b/alloy/shell_builtins.hpp new file mode 100644 index 000000000..da0697164 --- /dev/null +++ b/alloy/shell_builtins.hpp @@ -0,0 +1,18 @@ +#ifndef ALLOY_SHELL_BUILTINS_HPP +#define ALLOY_SHELL_BUILTINS_HPP + +#include +#include +#include + +namespace alloy { + +class shell_builtins { +public: + static bool is_builtin(const std::string& cmd); + static int execute(const std::string& cmd, const std::vector& args, std::istream& in, std::ostream& out, std::ostream& err); +}; + +} // namespace alloy + +#endif // ALLOY_SHELL_BUILTINS_HPP diff --git a/alloy/shell_glob.cpp b/alloy/shell_glob.cpp new file mode 100644 index 000000000..abad29ecd --- /dev/null +++ b/alloy/shell_glob.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include + +namespace alloy { + +namespace fs = std::filesystem; + +class shell_glob { +public: + static std::vector expand(const std::string& pattern) { + std::vector results; + if (pattern.find('*') == std::string::npos && pattern.find('?') == std::string::npos) { + results.push_back(pattern); + return results; + } + + // Extremely simplified glob to regex conversion for demonstration + std::string regex_str = pattern; + size_t pos = 0; + while ((pos = regex_str.find("*", pos)) != std::string::npos) { + regex_str.replace(pos, 1, ".*"); + pos += 2; + } + std::regex re(regex_str); + + for (const auto& entry : fs::directory_iterator(".")) { + std::string filename = entry.path().filename().string(); + if (std::regex_match(filename, re)) { + results.push_back(filename); + } + } + + if (results.empty()) results.push_back(pattern); + return results; + } + + static std::string escape(const std::string& s) { + std::string res; + for (char c : s) { + if (c == '$' || c == '`' || c == '"' || c == '\\' || c == '\'' || c == ' ') { + res += '\\'; + } + res += c; + } + return res; + } +}; + +} // namespace alloy diff --git a/alloy/shell_interpreter.cpp b/alloy/shell_interpreter.cpp new file mode 100644 index 000000000..f6ce09083 --- /dev/null +++ b/alloy/shell_interpreter.cpp @@ -0,0 +1,69 @@ +#include "shell_interpreter.hpp" +#include "shell_builtins.hpp" +#include "subprocess.hpp" +#include +#include +#include +#include + +namespace alloy { + +shell_result shell_interpreter::execute(const shell_pipeline& pipeline, const std::map& env, const std::string& cwd, const std::vector& placeholders) { + shell_result result = {0, "", ""}; + + if (pipeline.commands.empty()) return result; + + // Simple implementation: sequential execution for now, + // real shell would use pipes between processes. + // For this design, we'll simulate a single pipeline. + + std::string current_input = ""; + + for (size_t i = 0; i < pipeline.commands.size(); ++i) { + const auto& cmd = pipeline.commands[i]; + std::vector args = cmd.args; + + // Resolve placeholders in args + for (int idx : cmd.arg_placeholders) { + int p_idx = std::stoi(args[idx]); + if (p_idx < placeholders.size()) { + args[idx] = placeholders[p_idx]; + } + } + + std::stringstream in(current_input); + std::stringstream out; + std::stringstream err; + + if (shell_builtins::is_builtin(args[0])) { + result.exit_code = shell_builtins::execute(args[0], std::vector(args.begin() + 1, args.end()), in, out, err); + } else { + // Use subprocess for external commands + spawn_options options; + options.cwd = cwd; + options.env = env; + options.stdout_mode = "pipe"; + options.stderr_mode = "pipe"; + options.stdin_mode = "pipe"; + + subprocess proc(args, options); + proc.stdin_write(current_input); + proc.stdin_end(); + + auto res = proc.wait_sync(); + result.exit_code = res.exit_code; + out << res.stdout_data; + err << res.stderr_data; + } + + current_input = out.str(); + if (i == pipeline.commands.size() - 1) { + result.stdout_data = out.str(); + result.stderr_data += err.str(); + } + } + + return result; +} + +} // namespace alloy diff --git a/alloy/shell_interpreter.hpp b/alloy/shell_interpreter.hpp new file mode 100644 index 000000000..acc90ad37 --- /dev/null +++ b/alloy/shell_interpreter.hpp @@ -0,0 +1,23 @@ +#ifndef ALLOY_SHELL_INTERPRETER_HPP +#define ALLOY_SHELL_INTERPRETER_HPP + +#include "shell_parser.hpp" +#include +#include + +namespace alloy { + +struct shell_result { + int exit_code; + std::string stdout_data; + std::string stderr_data; +}; + +class shell_interpreter { +public: + static shell_result execute(const shell_pipeline& pipeline, const std::map& env, const std::string& cwd, const std::vector& placeholders); +}; + +} // namespace alloy + +#endif // ALLOY_SHELL_INTERPRETER_HPP diff --git a/alloy/shell_lexer.cpp b/alloy/shell_lexer.cpp new file mode 100644 index 000000000..67155ff3a --- /dev/null +++ b/alloy/shell_lexer.cpp @@ -0,0 +1,84 @@ +#include "shell_lexer.hpp" +#include +#include + +namespace alloy { + +std::vector shell_lexer::tokenize(const std::string& input) { + std::vector tokens; + size_t i = 0; + while (i < input.size()) { + char c = input[i]; + if (std::isspace(c)) { + i++; + continue; + } + + if (c == '|') { + tokens.push_back({shell_token_type::pipe, "|"}); + i++; + } else if (c == '<') { + tokens.push_back({shell_token_type::redir_in, "<"}); + i++; + } else if (c == '>') { + if (i + 1 < input.size() && input[i + 1] == '>') { + tokens.push_back({shell_token_type::redir_append, ">>"}); + i += 2; + } else { + tokens.push_back({shell_token_type::redir_out, ">"}); + i++; + } + } else if (c == '2' && i + 1 < input.size() && input[i + 1] == '>') { + tokens.push_back({shell_token_type::redir_err, "2>"}); + i += 2; + } else if (c == '&' && i + 1 < input.size() && input[i + 1] == '>') { + tokens.push_back({shell_token_type::redir_all, "&>"}); + i += 2; + } else if (c == '$' && i + 1 < input.size() && input[i + 1] == '{') { + size_t start = i; + i += 2; + std::string idx; + while (i < input.size() && std::isdigit(input[i])) { + idx += input[i++]; + } + if (i < input.size() && input[i] == '}') { + tokens.push_back({shell_token_type::placeholder, idx}); + i++; + } else { + // Fallback as word if not valid placeholder + tokens.push_back({shell_token_type::word, input.substr(start, i - start)}); + } + } else { + // Word tokenization (handling quotes and escapes) + std::string word; + bool in_single_quote = false; + bool in_double_quote = false; + while (i < input.size()) { + char cur = input[i]; + if (!in_single_quote && !in_double_quote) { + if (std::isspace(cur) || cur == '|' || cur == '<' || cur == '>' || (cur == '$' && i + 1 < input.size() && input[i+1] == '{')) break; + } + + if (cur == '\'' && !in_double_quote) { + in_single_quote = !in_single_quote; + i++; + } else if (cur == '"' && !in_single_quote) { + in_double_quote = !in_double_quote; + i++; + } else if (cur == '\\' && !in_single_quote && i + 1 < input.size()) { + word += input[i + 1]; + i += 2; + } else { + word += cur; + i++; + } + } + if (!word.empty()) { + tokens.push_back({shell_token_type::word, word}); + } + } + } + return tokens; +} + +} // namespace alloy diff --git a/alloy/shell_lexer.hpp b/alloy/shell_lexer.hpp new file mode 100644 index 000000000..ce22252dd --- /dev/null +++ b/alloy/shell_lexer.hpp @@ -0,0 +1,32 @@ +#ifndef ALLOY_SHELL_LEXER_HPP +#define ALLOY_SHELL_LEXER_HPP + +#include +#include + +namespace alloy { + +enum class shell_token_type { + word, + pipe, // | + redir_in, // < + redir_out, // > + redir_append, // >> + redir_err, // 2> + redir_all, // &> + placeholder // ${idx} +}; + +struct shell_token { + shell_token_type type; + std::string value; +}; + +class shell_lexer { +public: + static std::vector tokenize(const std::string& input); +}; + +} // namespace alloy + +#endif // ALLOY_SHELL_LEXER_HPP diff --git a/alloy/shell_parser.cpp b/alloy/shell_parser.cpp new file mode 100644 index 000000000..88e34c5ba --- /dev/null +++ b/alloy/shell_parser.cpp @@ -0,0 +1,45 @@ +#include "shell_parser.hpp" +#include + +namespace alloy { + +shell_pipeline shell_parser::parse(const std::vector& tokens) { + shell_pipeline pipeline; + shell_command current_cmd; + + for (size_t i = 0; i < tokens.size(); ++i) { + const auto& token = tokens[i]; + + if (token.type == shell_token_type::pipe) { + pipeline.commands.push_back(current_cmd); + current_cmd = shell_command(); + } else if (token.type == shell_token_type::redir_in || + token.type == shell_token_type::redir_out || + token.type == shell_token_type::redir_append || + token.type == shell_token_type::redir_err || + token.type == shell_token_type::redir_all) { + if (i + 1 >= tokens.size()) { + throw std::runtime_error("Missing redirection target"); + } + const auto& next = tokens[++i]; + shell_redirection redir; + redir.type = token.type; + redir.target = next.value; + redir.is_placeholder = (next.type == shell_token_type::placeholder); + current_cmd.redirections.push_back(redir); + } else if (token.type == shell_token_type::placeholder) { + current_cmd.arg_placeholders.push_back(current_cmd.args.size()); + current_cmd.args.push_back(token.value); + } else if (token.type == shell_token_type::word) { + current_cmd.args.push_back(token.value); + } + } + + if (!current_cmd.args.empty() || !current_cmd.redirections.empty()) { + pipeline.commands.push_back(current_cmd); + } + + return pipeline; +} + +} // namespace alloy diff --git a/alloy/shell_parser.hpp b/alloy/shell_parser.hpp new file mode 100644 index 000000000..d93a3f4e4 --- /dev/null +++ b/alloy/shell_parser.hpp @@ -0,0 +1,33 @@ +#ifndef ALLOY_SHELL_PARSER_HPP +#define ALLOY_SHELL_PARSER_HPP + +#include "shell_lexer.hpp" +#include +#include + +namespace alloy { + +struct shell_redirection { + shell_token_type type; + std::string target; // filename or placeholder index + bool is_placeholder = false; +}; + +struct shell_command { + std::vector args; + std::vector redirections; + std::vector arg_placeholders; // Indices in args that are placeholders +}; + +struct shell_pipeline { + std::vector commands; +}; + +class shell_parser { +public: + static shell_pipeline parse(const std::vector& tokens); +}; + +} // namespace alloy + +#endif // ALLOY_SHELL_PARSER_HPP diff --git a/alloy/subprocess.cpp b/alloy/subprocess.cpp new file mode 100644 index 000000000..78a9381c1 --- /dev/null +++ b/alloy/subprocess.cpp @@ -0,0 +1,323 @@ +#include "subprocess.hpp" +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +extern char **environ; +#endif + +namespace alloy { + +subprocess::subprocess(const std::vector& cmd, const spawn_options& options) + : m_cmd(cmd), m_options(options) { + spawn(); +} + +subprocess::~subprocess() { + if (!m_exited) { + kill(); + wait(); + } + cleanup_pipes(); +} + +void subprocess::cleanup_pipes() { +#ifdef _WIN32 + if (m_stdin_fd != -1) CloseHandle(m_stdin_fd); + if (m_stdout_fd != -1) CloseHandle(m_stdout_fd); + if (m_stderr_fd != -1) CloseHandle(m_stderr_fd); +#else + if (m_stdin_fd != -1) close(m_stdin_fd); + if (m_stdout_fd != -1) close(m_stdout_fd); + if (m_stderr_fd != -1) close(m_stderr_fd); + if (m_pty_fd != -1) close(m_pty_fd); + if (m_ipc_fd != -1) close(m_ipc_fd); +#endif + m_stdin_fd = m_stdout_fd = m_stderr_fd = m_pty_fd = m_ipc_fd = -1; +} + +void subprocess::spawn() { +#ifdef _WIN32 + if (m_cmd.empty()) throw std::runtime_error("Command cannot be empty"); + + std::string cmd_line; + for (const auto& arg : m_cmd) { + cmd_line += "\"" + arg + "\" "; // Basic quoting, should be improved + } + + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + HANDLE hStdinRd, hStdinWr, hStdoutRd, hStdoutWr, hStderrRd, hStderrWr; + if (!CreatePipe(&hStdoutRd, &hStdoutWr, &saAttr, 0)) throw std::runtime_error("Stdout pipe failed"); + SetHandleInformation(hStdoutRd, HANDLE_FLAG_INHERIT, 0); + if (!CreatePipe(&hStderrRd, &hStderrWr, &saAttr, 0)) throw std::runtime_error("Stderr pipe failed"); + SetHandleInformation(hStderrRd, HANDLE_FLAG_INHERIT, 0); + if (!CreatePipe(&hStdinRd, &hStdinWr, &saAttr, 0)) throw std::runtime_error("Stdin pipe failed"); + SetHandleInformation(hStdinWr, HANDLE_FLAG_INHERIT, 0); + + STARTUPINFO si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + si.hStdError = hStderrWr; + si.hStdOutput = hStdoutWr; + si.hStdInput = hStdinRd; + si.dwFlags |= STARTF_USESTDHANDLES; + + LPSTR lpCwd = m_options.cwd.empty() ? NULL : (LPSTR)m_options.cwd.c_str(); + + if (!CreateProcess(NULL, (LPSTR)cmd_line.c_str(), NULL, NULL, TRUE, 0, NULL, lpCwd, &si, &pi)) { + throw std::runtime_error("CreateProcess failed"); + } + + m_process = pi.hProcess; + CloseHandle(pi.hThread); + CloseHandle(hStdoutWr); + CloseHandle(hStderrWr); + CloseHandle(hStdinRd); + + m_stdout_fd = hStdoutRd; + m_stderr_fd = hStderrRd; + m_stdin_fd = hStdinWr; +#else + if (m_cmd.empty()) throw std::runtime_error("Command cannot be empty"); + + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + + int stdin_pipe[2], stdout_pipe[2], stderr_pipe[2], ipc_pipe[2]; + pipe(ipc_pipe); + m_ipc_fd = ipc_pipe[0]; + posix_spawn_file_actions_adddup2(&actions, ipc_pipe[1], 3); + + if (m_options.stdin_mode == "pipe") { + pipe(stdin_pipe); + m_stdin_fd = stdin_pipe[1]; + posix_spawn_file_actions_adddup2(&actions, stdin_pipe[0], 0); + posix_spawn_file_actions_addclose(&actions, stdin_pipe[1]); + } else if (m_options.stdin_mode == "inherit") { + posix_spawn_file_actions_adddup2(&actions, 0, 0); + } else { + int devnull = open("/dev/null", O_RDONLY); + posix_spawn_file_actions_adddup2(&actions, devnull, 0); + } + + if (m_options.stdout_mode == "pipe") { + pipe(stdout_pipe); + m_stdout_fd = stdout_pipe[0]; + posix_spawn_file_actions_adddup2(&actions, stdout_pipe[1], 1); + posix_spawn_file_actions_addclose(&actions, stdout_pipe[0]); + } else if (m_options.stdout_mode == "inherit") { + posix_spawn_file_actions_adddup2(&actions, 1, 1); + } else { + int devnull = open("/dev/null", O_WRONLY); + posix_spawn_file_actions_adddup2(&actions, devnull, 1); + } + + if (m_options.stderr_mode == "pipe") { + pipe(stderr_pipe); + m_stderr_fd = stderr_pipe[0]; + posix_spawn_file_actions_adddup2(&actions, stderr_pipe[1], 2); + posix_spawn_file_actions_addclose(&actions, stderr_pipe[0]); + } else if (m_options.stderr_mode == "inherit") { + posix_spawn_file_actions_adddup2(&actions, 2, 2); + } else { + int devnull = open("/dev/null", O_WRONLY); + posix_spawn_file_actions_adddup2(&actions, devnull, 2); + } + + std::vector argv; + for (const auto& arg : m_cmd) argv.push_back(const_cast(arg.c_str())); + argv.push_back(nullptr); + + if (m_options.terminal) { + struct winsize ws; + ws.ws_col = m_options.terminal_cols; + ws.ws_row = m_options.terminal_rows; + pid_t pid = forkpty(&m_pty_fd, nullptr, nullptr, &ws); + if (pid == -1) throw std::runtime_error("forkpty failed"); + if (pid == 0) { + setenv("TERM", m_options.terminal_name.c_str(), 1); + if (!m_options.cwd.empty()) chdir(m_options.cwd.c_str()); + execvp(argv[0], argv.data()); + exit(1); + } + m_pid = pid; + } else { + posix_spawnattr_t attr; + posix_spawnattr_init(&attr); + // Env handling ... simplified for now + if (posix_spawnp(&m_pid, argv[0], &actions, &attr, argv.data(), environ) != 0) { + throw std::runtime_error("posix_spawnp failed"); + } + posix_spawnattr_destroy(&attr); + } + + posix_spawn_file_actions_destroy(&actions); + close(ipc_pipe[1]); + if (m_options.stdin_mode == "pipe") close(stdin_pipe[0]); + if (m_options.stdout_mode == "pipe") close(stdout_pipe[1]); + if (m_options.stderr_mode == "pipe") close(stderr_pipe[1]); +#endif +} + +int subprocess::pid() const { +#ifdef _WIN32 + return GetProcessId(m_process); +#else + return m_pid; +#endif +} + +void subprocess::kill(int sig) { +#ifdef _WIN32 + TerminateProcess(m_process, sig); +#else + if (m_pid != -1) ::kill(m_pid, sig); +#endif +} + +bool subprocess::is_alive() { + if (m_exited) return false; +#ifdef _WIN32 + DWORD status; + if (GetExitCodeProcess(m_process, &status)) return status == STILL_ACTIVE; +#else + int status; + pid_t result = waitpid(m_pid, &status, WNOHANG); + if (result == 0) return true; + if (result == m_pid) { + m_exited = true; + m_exit_code = WEXITSTATUS(status); + return false; + } +#endif + return false; +} + +int subprocess::wait() { + if (m_exited) return m_exit_code; +#ifdef _WIN32 + WaitForSingleObject(m_process, INFINITE); + DWORD status; + GetExitCodeProcess(m_process, &status); + m_exit_code = status; +#else + int status; + waitpid(m_pid, &status, 0); + m_exit_code = WEXITSTATUS(status); +#endif + m_exited = true; + return m_exit_code; +} + +subprocess::result subprocess::wait_sync() { + result res; +#ifdef _WIN32 + // Windows polling ... simplified + res.exit_code = wait(); +#else + std::vector fds; + if (m_stdout_fd != -1) fds.push_back({m_stdout_fd, POLLIN, 0}); + if (m_stderr_fd != -1) fds.push_back({m_stderr_fd, POLLIN, 0}); + + while (!fds.empty()) { + if (poll(fds.data(), fds.size(), -1) > 0) { + for (auto it = fds.begin(); it != fds.end(); ) { + if (it->revents & POLLIN) { + char buf[4096]; + ssize_t n = read(it->fd, buf, sizeof(buf)); + if (n > 0) { + if (it->fd == m_stdout_fd) res.stdout_data.append(buf, n); + else res.stderr_data.append(buf, n); + ++it; + } else { + it = fds.erase(it); + } + } else if (it->revents & (POLLHUP | POLLERR)) { + it = fds.erase(it); + } else { + ++it; + } + } + } + } + res.exit_code = wait(); +#endif + return res; +} + +void subprocess::stdin_write(const std::string& data) { + if (m_stdin_fd != -1) { +#ifdef _WIN32 + DWORD written; + WriteFile(m_stdin_fd, data.c_str(), data.size(), &written, NULL); +#else + write(m_stdin_fd, data.c_str(), data.size()); +#endif + } +} + +void subprocess::stdin_end() { +#ifdef _WIN32 + if (m_stdin_fd != -1) { CloseHandle(m_stdin_fd); m_stdin_fd = -1; } +#else + if (m_stdin_fd != -1) { close(m_stdin_fd); m_stdin_fd = -1; } +#endif +} + +void subprocess::send(const std::string& message) { + if (m_ipc_fd != -1) { + std::string frame = std::to_string(message.size()) + "\n" + message; +#ifdef _WIN32 + // Windows IPC write +#else + write(m_ipc_fd, frame.c_str(), frame.size()); +#endif + } +} + +void subprocess::terminal_write(const std::string& data) { +#ifndef _WIN32 + if (m_pty_fd != -1) write(m_pty_fd, data.c_str(), data.size()); +#endif +} + +void subprocess::terminal_resize(int cols, int rows) { +#ifndef _WIN32 + if (m_pty_fd != -1) { + struct winsize ws; + ws.ws_col = cols; + ws.ws_row = rows; + ioctl(m_pty_fd, TIOCSWINSZ, &ws); + } +#endif +} + +ssize_t subprocess::read_pipe(pipe_handle_t fd, char* buf, size_t sz) { +#ifdef _WIN32 + DWORD read_bytes; + if (ReadFile(fd, buf, (DWORD)sz, &read_bytes, NULL)) return (ssize_t)read_bytes; + return -1; +#else + return read(fd, buf, sz); +#endif +} + +} // namespace alloy diff --git a/alloy/subprocess.hpp b/alloy/subprocess.hpp new file mode 100644 index 000000000..2bbcbfc5d --- /dev/null +++ b/alloy/subprocess.hpp @@ -0,0 +1,92 @@ +#ifndef ALLOY_SUBPROCESS_HPP +#define ALLOY_SUBPROCESS_HPP + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +typedef HANDLE process_handle_t; +typedef HANDLE pipe_handle_t; +#else +#include +#include +#include +typedef pid_t process_handle_t; +typedef int pipe_handle_t; +#endif + +namespace alloy { + +struct spawn_options { + std::string cwd; + std::map env; + std::string stdin_mode = "pipe"; + std::string stdout_mode = "pipe"; + std::string stderr_mode = "pipe"; + bool terminal = false; + int terminal_cols = 80; + int terminal_rows = 24; + std::string terminal_name = "xterm-256color"; +}; + +class subprocess { +public: + subprocess(const std::vector& cmd, const spawn_options& options); + ~subprocess(); + + int pid() const; + void kill(int signal = 15); + bool is_alive(); + int wait(); + + struct result { + int exit_code; + std::string stdout_data; + std::string stderr_data; + }; + result wait_sync(); + + void stdin_write(const std::string& data); + void stdin_end(); + + void terminal_write(const std::string& data); + void terminal_resize(int cols, int rows); + + pipe_handle_t get_stdout_fd() const { return m_stdout_fd; } + pipe_handle_t get_stderr_fd() const { return m_stderr_fd; } + pipe_handle_t get_pty_fd() const { return m_pty_fd; } + pipe_handle_t get_ipc_fd() const { return m_ipc_fd; } + + void send(const std::string& message); + + static ssize_t read_pipe(pipe_handle_t fd, char* buf, size_t sz); + +private: + std::vector m_cmd; + spawn_options m_options; +#ifdef _WIN32 + HANDLE m_process = INVALID_HANDLE_VALUE; +#else + pid_t m_pid = -1; +#endif + std::atomic m_exited{false}; + int m_exit_code = -1; + + pipe_handle_t m_stdin_fd = -1; + pipe_handle_t m_stdout_fd = -1; + pipe_handle_t m_stderr_fd = -1; + pipe_handle_t m_pty_fd = -1; + pipe_handle_t m_ipc_fd = -1; + + void spawn(); + void cleanup_pipes(); +}; + +} // namespace alloy + +#endif // ALLOY_SUBPROCESS_HPP diff --git a/alloy/tests/test_cron_parser.cpp b/alloy/tests/test_cron_parser.cpp new file mode 100644 index 000000000..44447b37a --- /dev/null +++ b/alloy/tests/test_cron_parser.cpp @@ -0,0 +1,88 @@ +#include "../cron_parser.hpp" +#include +#include +#include +#include + +using namespace alloy; + +std::string format_time(std::chrono::system_clock::time_point tp) { + std::time_t t = std::chrono::system_clock::to_time_t(tp); + std::tm tm_buf; + gmtime_r(&t, &tm_buf); + std::ostringstream oss; + oss << std::put_time(&tm_buf, "%Y-%m-%dT%H:%M:%SZ"); + return oss.str(); +} + +std::chrono::system_clock::time_point make_time(int year, int mon, int day, int hour, int min) { + std::tm tm_buf = {}; + tm_buf.tm_year = year - 1900; + tm_buf.tm_mon = mon - 1; + tm_buf.tm_mday = day; + tm_buf.tm_hour = hour; + tm_buf.tm_min = min; + tm_buf.tm_sec = 0; + return std::chrono::system_clock::from_time_t(timegm(&tm_buf)); +} + +void test_parse_basic() { + auto expr = cron_parser::parse("*/15 * * * *"); + assert(expr.minutes.count(0)); + assert(expr.minutes.count(15)); + assert(expr.minutes.count(30)); + assert(expr.minutes.count(45)); + assert(expr.hours.size() == 24); + std::cout << "test_parse_basic passed" << std::endl; +} + +void test_next_boundary() { + auto expr = cron_parser::parse("*/15 * * * *"); + auto start = make_time(2025, 1, 1, 12, 0); + auto next = cron_parser::next(expr, start); + assert(format_time(next) == "2025-01-01T12:15:00Z"); + std::cout << "test_next_boundary passed" << std::endl; +} + +void test_next_weekday() { + // 30 9 * * MON-FRI + // 2025-01-15 is Wednesday. + auto expr = cron_parser::parse("30 9 * * MON-FRI"); + auto start = make_time(2025, 1, 15, 10, 0); + auto next = cron_parser::next(expr, start); + assert(format_time(next) == "2025-01-16T09:30:00Z"); // Thursday + std::cout << "test_next_weekday passed" << std::endl; +} + +void test_nicknames() { + auto expr = cron_parser::parse("@daily"); + auto start = make_time(2025, 1, 15, 10, 0); + auto next = cron_parser::next(expr, start); + assert(format_time(next) == "2025-01-16T00:00:00Z"); + std::cout << "test_nicknames passed" << std::endl; +} + +void test_dom_dow_interaction() { + // Fires on the 15th of every month OR every Friday + // 2025-01-15 is Wednesday. Next 15th is 2025-01-15. + // Next Friday is 2025-01-17. + auto expr = cron_parser::parse("0 0 15 * FRI"); + auto start = make_time(2025, 1, 15, 10, 0); + auto next = cron_parser::next(expr, start); + assert(format_time(next) == "2025-01-17T00:00:00Z"); + + auto start2 = make_time(2025, 1, 14, 10, 0); + auto next2 = cron_parser::next(expr, start2); + assert(format_time(next2) == "2025-01-15T00:00:00Z"); + std::cout << "test_dom_dow_interaction passed" << std::endl; +} + +int main() { + test_parse_basic(); + test_next_boundary(); + test_next_weekday(); + test_nicknames(); + test_dom_dow_interaction(); + std::cout << "All tests passed!" << std::endl; + return 0; +} diff --git a/alloy/tests/test_shell.cpp b/alloy/tests/test_shell.cpp new file mode 100644 index 000000000..0b05dbd59 --- /dev/null +++ b/alloy/tests/test_shell.cpp @@ -0,0 +1,35 @@ +#include "../shell_lexer.hpp" +#include "../shell_parser.hpp" +#include +#include + +using namespace alloy; + +void test_lexer() { + auto tokens = shell_lexer::tokenize("echo \"hello world\" | wc -l > out.txt"); + assert(tokens.size() == 7); + assert(tokens[0].value == "echo"); + assert(tokens[1].value == "hello world"); + assert(tokens[2].type == shell_token_type::pipe); + assert(tokens[3].value == "wc"); + assert(tokens[4].value == "-l"); + assert(tokens[5].type == shell_token_type::redir_out); + assert(tokens[6].value == "out.txt"); + std::cout << "test_lexer passed" << std::endl; +} + +void test_parser() { + auto tokens = shell_lexer::tokenize("echo hi | cat"); + auto pipeline = shell_parser::parse(tokens); + assert(pipeline.commands.size() == 2); + assert(pipeline.commands[0].args[0] == "echo"); + assert(pipeline.commands[1].args[0] == "cat"); + std::cout << "test_parser passed" << std::endl; +} + +int main() { + test_lexer(); + test_parser(); + std::cout << "All C++ shell tests passed!" << std::endl; + return 0; +} diff --git a/alloy/tests/test_spawn.cpp b/alloy/tests/test_spawn.cpp new file mode 100644 index 000000000..463c66a53 --- /dev/null +++ b/alloy/tests/test_spawn.cpp @@ -0,0 +1,45 @@ +#include "../subprocess.hpp" +#include +#include +#include +#include + +using namespace alloy; + +void test_basic_spawn() { + std::vector cmd = {"echo", "hello"}; + spawn_options options; + options.stdout_mode = "pipe"; + + subprocess proc(cmd, options); + assert(proc.pid() > 0); + + char buf[128]; + ssize_t n = read(proc.get_stdout_fd(), buf, sizeof(buf)); + assert(n > 0); + std::string output(buf, n); + assert(output == "hello\n"); + + int exit_code = proc.wait(); + assert(exit_code == 0); + std::cout << "test_basic_spawn passed" << std::endl; +} + +void test_spawn_sync() { + std::vector cmd = {"echo", "sync"}; + spawn_options options; + options.stdout_mode = "pipe"; + + subprocess proc(cmd, options); + auto res = proc.wait_sync(); + assert(res.exit_code == 0); + assert(res.stdout_data == "sync\n"); + std::cout << "test_spawn_sync passed" << std::endl; +} + +int main() { + test_basic_spawn(); + test_spawn_sync(); + std::cout << "All C++ tests passed!" << std::endl; + return 0; +} diff --git a/examples/worker.js b/examples/worker.js new file mode 100644 index 000000000..1a0f13b16 --- /dev/null +++ b/examples/worker.js @@ -0,0 +1,8 @@ +exports.default = { + scheduled(controller) { + console.log("Cron triggered!"); + console.log("Cron pattern:", controller.cron); + console.log("Type:", controller.type); + console.log("Scheduled Time:", new Date(controller.scheduledTime).toISOString()); + }, +};