Compare commits

..

No commits in common. "06a6567da993135cabe814876977f5f32a0cb25c" and "5ed7b459c83e684d66ff1db42a883f7556eea057" have entirely different histories.

15 changed files with 890 additions and 623 deletions

View File

@ -40,12 +40,14 @@ set(UUID_LIB "uuid")
# 1. The main library: lubuntuci_lib # 1. The main library: lubuntuci_lib
# #
add_library(lubuntuci_lib SHARED add_library(lubuntuci_lib SHARED
common.cpp
utilities.cpp utilities.cpp
db_common.cpp db_common.cpp
git_common.cpp git_common.cpp
sources_parser.cpp sources_parser.cpp
ci_logic.cpp ci_logic.cpp
ci_database_objs.cpp ci_database_objs.cpp
lubuntuci_lib.cpp
task_queue.cpp task_queue.cpp
template_renderer.cpp template_renderer.cpp
web_server.cpp web_server.cpp

View File

@ -43,16 +43,17 @@ std::vector<Release> Release::get_releases() {
QString query_str = "SELECT id, version, codename, isDefault FROM release;"; QString query_str = "SELECT id, version, codename, isDefault FROM release;";
QSqlQuery query(query_str, get_thread_connection()); QSqlQuery query(query_str, get_thread_connection());
while (query.next()) { while (query.next()) {
result.emplace_back(Release(query.value("id").toInt(), Release current_release(query.value("id").toInt(), query.value("version").toInt(),
query.value("version").toInt(), query.value("codename").toString().toStdString(),
query.value("codename").toString().toStdString(), query.value("isDefault").toBool());
query.value("isDefault").toBool())); result.emplace_back(current_release);
} }
return result; return result;
} }
Release Release::get_release_by_id(int id) { Release Release::get_release_by_id(int id) {
QSqlQuery query(get_thread_connection()); QSqlQuery query(get_thread_connection());
query.prepare("SELECT id, version, codename, isDefault FROM release WHERE id = ? LIMIT 1"); query.prepare("SELECT id, version, codename, isDefault FROM release WHERE id = ? LIMIT 1");
query.bindValue(0, id); query.bindValue(0, id);
if (!ci_query_exec(&query)) { if (!ci_query_exec(&query)) {
@ -60,10 +61,15 @@ Release Release::get_release_by_id(int id) {
return Release(); return Release();
} }
if (query.next()) { if (query.next()) {
return Release(query.value(0).toInt(), int release_id = query.value(0).toInt();
query.value(1).toInt(), int version = query.value(1).toInt();
query.value(2).toString().toStdString(), QString codename = query.value(2).toString();
query.value(3).toBool()); bool isDefault = query.value(3).toBool();
// Create and return the Release object
return Release(release_id, version, codename.toStdString(), isDefault);
} else {
std::cout << "No release found for ID: " << id << "\n";
} }
// Return an empty Release object if no match is found // Return an empty Release object if no match is found
@ -150,12 +156,12 @@ Package Package::get_package_by_id(int id) {
return Package(); return Package();
} }
if (query.next()) { if (query.next()) {
return Package(query.value("id").toInt(), Package current_package(query.value("id").toInt(), query.value("name").toString().toStdString(),
query.value("name").toString().toStdString(), query.value("large").toBool(),
query.value("large").toBool(), query.value("upstream_url").toString().toStdString(),
query.value("upstream_url").toString().toStdString(), query.value("packaging_branch").toString().toStdString(),
query.value("packaging_branch").toString().toStdString(), query.value("packaging_url").toString().toStdString());
query.value("packaging_url").toString().toStdString()); return current_package;
} }
return Package(); return Package();
} }
@ -230,12 +236,22 @@ bool Package::set_packages(YAML::Node& packages) {
return true; return true;
} }
std::string Package::transform_url(const std::string &url) { std::string Package::transform_url(const std::string& url) {
// Precompiled regex patterns and their replacements
static const std::vector<std::pair<std::regex, std::string>> patterns = { static const std::vector<std::pair<std::regex, std::string>> patterns = {
// git.launchpad.net: Append "/commit/?id="
{ std::regex(R"(^(https://git\.launchpad\.net/.*)$)"), "$1/commit/?id=" }, { std::regex(R"(^(https://git\.launchpad\.net/.*)$)"), "$1/commit/?id=" },
// code.qt.io: Replace "/qt/" with "/cgit/qt/" and append "/commit/?id="
{ std::regex(R"(^https://code\.qt\.io/qt/([^/]+\.git)$)"), "https://code.qt.io/cgit/qt/$1/commit/?id=" }, { std::regex(R"(^https://code\.qt\.io/qt/([^/]+\.git)$)"), "https://code.qt.io/cgit/qt/$1/commit/?id=" },
// invent.kde.org: Replace ".git" with "/-/commit/"
{ std::regex(R"(^https://invent\.kde\.org/([^/]+/[^/]+)\.git$)"), "https://invent.kde.org/$1/-/commit/" }, { std::regex(R"(^https://invent\.kde\.org/([^/]+/[^/]+)\.git$)"), "https://invent.kde.org/$1/-/commit/" },
// git.lubuntu.me: Replace ".git" with "/commit/"
{ std::regex(R"(^https://git\.lubuntu\.me/([^/]+/[^/]+)\.git$)"), "https://git.lubuntu.me/$1/commit/" }, { std::regex(R"(^https://git\.lubuntu\.me/([^/]+/[^/]+)\.git$)"), "https://git.lubuntu.me/$1/commit/" },
// gitlab.kitware.com: Replace ".git" with "/-/commit/"
{ std::regex(R"(^https://gitlab\.kitware\.com/([^/]+/[^/]+)\.git$)"), "https://gitlab.kitware.com/$1/-/commit/" }, { std::regex(R"(^https://gitlab\.kitware\.com/([^/]+/[^/]+)\.git$)"), "https://gitlab.kitware.com/$1/-/commit/" },
}; };
@ -263,10 +279,10 @@ std::vector<Branch> Branch::get_branches() {
QString query_str = "SELECT id, name, upload_target, upload_target_ssh FROM branch"; QString query_str = "SELECT id, name, upload_target, upload_target_ssh FROM branch";
QSqlQuery query(query_str, get_thread_connection()); QSqlQuery query(query_str, get_thread_connection());
while (query.next()) { while (query.next()) {
result.emplace_back(Branch(query.value("id").toInt(), Branch current_branch(query.value("id").toInt(), query.value("name").toString().toStdString(),
query.value("name").toString().toStdString(), query.value("upload_target").toString().toStdString(),
query.value("upload_target").toString().toStdString(), query.value("upload_target_ssh").toString().toStdString());
query.value("upload_target_ssh").toString().toStdString())); result.emplace_back(current_branch);
} }
return result; return result;
} }
@ -881,37 +897,81 @@ void PackageConf::sync() {
} }
bool PackageConf::can_check_source_upload() { bool PackageConf::can_check_source_upload() {
std::lock_guard<std::mutex> lock(*task_mutex_); int _total_task_count = total_task_count();
bool upload_ok = false, source_check_ok = false; if (_total_task_count == 0) return false;
std::int64_t upload_time = 0, source_check_time = 0;
for (const auto &kv : jobstatus_task_map_) { std::int64_t upload_timestamp = 0;
if (kv.first->name == "upload" && kv.second && kv.second->finish_time > 0 && kv.second->successful) { std::int64_t source_check_timestamp = 0;
upload_ok = true; std::set<std::string> valid_successful_statuses = {"pull", "tarball", "source_build", "upload"};
upload_time = kv.second->finish_time; {
} std::lock_guard<std::mutex> lock(*task_mutex_);
if (kv.first->name == "source_check" && kv.second && kv.second->finish_time > 0 && kv.second->successful) { for (auto &kv : jobstatus_task_map_) {
source_check_ok = true; auto &jobstatus = kv.first;
source_check_time = kv.second->finish_time; auto &task_ptr = kv.second;
if (valid_successful_statuses.contains(jobstatus->name)) _total_task_count--;
if (jobstatus->name == "upload" && task_ptr && task_ptr->successful) {
upload_timestamp = task_ptr->finish_time;
continue;
}
if (jobstatus->name == "source_check" && task_ptr) {
source_check_timestamp = task_ptr->finish_time;
_total_task_count--;
continue;
}
} }
} }
return upload_ok && source_check_ok && (source_check_time <= upload_time); bool all_req_tasks_present = _total_task_count == 0;
if (!all_req_tasks_present || (upload_timestamp == 0 && source_check_timestamp == 0)) {
return false;
} else if (all_req_tasks_present && upload_timestamp != 0 && source_check_timestamp == 0) {
return true;
} else if (all_req_tasks_present) {
return source_check_timestamp <= upload_timestamp;
}
return false;
} }
bool PackageConf::can_check_builds() { bool PackageConf::can_check_builds() {
std::lock_guard<std::mutex> lock(*task_mutex_); // In this case, the source check should be first
bool upload_ok = false, build_check_ok = false; if (can_check_source_upload()) return false;
std::int64_t upload_time = 0, build_check_time = 0; int _total_task_count = total_task_count();
for (const auto &kv : jobstatus_task_map_) { if (_total_task_count == 0) return false;
if (kv.first->name == "upload" && kv.second && kv.second->finish_time > 0 && kv.second->successful) {
upload_ok = true; std::int64_t upload_timestamp = 0;
upload_time = kv.second->finish_time; std::int64_t binary_check_timestamp = 0;
} std::set<std::string> valid_successful_statuses = {"pull", "tarball", "source_build", "upload", "source_check"};
if (kv.first->name == "build_check" && kv.second && kv.second->finish_time > 0 && kv.second->successful) { {
build_check_ok = true; std::lock_guard<std::mutex> lock(*task_mutex_);
build_check_time = kv.second->finish_time; for (auto &kv : jobstatus_task_map_) {
auto &jobstatus = kv.first;
auto &task_ptr = kv.second;
if (valid_successful_statuses.contains(jobstatus->name)) _total_task_count--;
if (jobstatus->name == "upload" && task_ptr && task_ptr->successful) {
upload_timestamp = task_ptr->finish_time;
continue;
}
if (jobstatus->name == "build_check" && task_ptr) {
binary_check_timestamp = task_ptr->finish_time;
_total_task_count--;
continue;
}
} }
} }
return upload_ok && build_check_ok && (build_check_time <= upload_time); bool all_req_tasks_present = _total_task_count == 0;
if (!all_req_tasks_present || (upload_timestamp == 0 && binary_check_timestamp == 0)) {
return false;
} else if (all_req_tasks_present && upload_timestamp != 0 && binary_check_timestamp == 0) {
return true;
} else if (all_req_tasks_present) {
return binary_check_timestamp <= upload_timestamp;
}
return false;
} }
// End of PackageConf // End of PackageConf
// Start of GitCommit // Start of GitCommit

View File

@ -27,7 +27,7 @@
#include <QSqlDatabase> #include <QSqlDatabase>
#include <yaml-cpp/yaml.h> #include <yaml-cpp/yaml.h>
#include "utilities.h" #include "common.h"
class Person { class Person {
public: public:

View File

@ -6,7 +6,7 @@
// (at your option) any later version. // (at your option) any later version.
// //
// This program is distributed in the hope that it will be useful, // This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY, without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
// //
@ -15,6 +15,8 @@
#include "task_queue.h" #include "task_queue.h"
#include "ci_logic.h" #include "ci_logic.h"
#include "lubuntuci_lib.h"
#include "common.h"
#include "utilities.h" #include "utilities.h"
#include "db_common.h" #include "db_common.h"
#include "git_common.h" #include "git_common.h"
@ -528,7 +530,7 @@ void CiLogic::process_entire_pipeline(std::shared_ptr<PackageConf> &proj,
bool pull_success = pull_project(proj); bool pull_success = pull_project(proj);
bool tarball_success = create_project_tarball(proj); bool tarball_success = create_project_tarball(proj);
const auto [build_success, changes_files] = build_project(proj); const auto [build_success, changes_files] = build_project(proj);
upload_and_lint(proj, changes_files, skip_dput, std::make_shared<Log>()); upload_and_lint(proj, changes_files, skip_dput);
do_summary(skip_cleanup); do_summary(skip_cleanup);
log_info("Pipeline done for " + proj->package->name); log_info("Pipeline done for " + proj->package->name);
} catch(std::exception &ex) { } catch(std::exception &ex) {
@ -827,36 +829,3 @@ std::string CiLogic::get_task_log(int task_id) {
} }
return ""; return "";
} }
std::vector<std::shared_ptr<PackageConf>> CiLogic::list_known_repos(int page,
int per_page,
const std::string &sort_by,
const std::string &sort_order) {
init_global();
if (page != 0 && per_page != 0 && !sort_by.empty() && !sort_order.empty()) return get_config("", page, per_page, sort_by, sort_order);
else return get_config();
}
bool CiLogic::pull_repo_by_name(const std::string &repo_name, std::shared_ptr<Log> log) {
init_global();
auto pkgconfs = get_config(repo_name);
if (pkgconfs.empty()) return false;
return pull_project(pkgconfs.at(0), log);
}
bool CiLogic::create_project_tarball_by_name(const std::string &repo_name, std::shared_ptr<Log> log) {
init_global();
auto pkgconfs = get_config(repo_name);
if (pkgconfs.empty()) return false;
return create_project_tarball(pkgconfs.at(0), log);
}
bool CiLogic::build_repo_by_name(const std::string &repo_name, std::shared_ptr<Log> log) {
init_global();
bool success = true;
for (auto pkgconf : get_config(repo_name)) {
auto [build_ok, changes_files] = build_project(pkgconf, log);
success = success && build_ok && upload_and_lint(pkgconf, changes_files, false, log);
}
return success;
}

View File

@ -32,8 +32,11 @@
#include <QSqlDatabase> #include <QSqlDatabase>
#include <yaml-cpp/yaml.h> #include <yaml-cpp/yaml.h>
namespace fs = std::filesystem; struct CiProject;
/**
* Data describing one package to pull/build/etc.
*/
struct CiProject { struct CiProject {
std::string name; std::string name;
std::string version; std::string version;
@ -42,98 +45,78 @@ struct CiProject {
std::string upstream_url; std::string upstream_url;
std::string packaging_url; std::string packaging_url;
std::optional<std::string> packaging_branch; std::optional<std::string> packaging_branch;
fs::path main_tarball; std::filesystem::path main_tarball;
bool large = false; bool large = false;
// These get populated during build:
// These get populated during build
std::vector<std::string> changes_files; std::vector<std::string> changes_files;
std::vector<std::string> devel_changes_files; std::vector<std::string> devel_changes_files;
}; };
class CiLogic { class CiLogic {
public: public:
// Initialize global config and database // Initialize global configurations
void init_global(); void init_global();
// Load YAML config from a given path // Load YAML configuration from a given path
YAML::Node load_yaml_config(const fs::path &config_path); YAML::Node load_yaml_config(const std::filesystem::path &config_path);
// Convert a YAML node to a CiProject // Convert a YAML node to a CiProject structure
CiProject yaml_to_project(const YAML::Node &pkg_node); CiProject yaml_to_project(const YAML::Node &pkg_node);
// Pipeline functions bool pull_project(std::shared_ptr<PackageConf> &proj, std::shared_ptr<Log> log = NULL);
bool pull_project(std::shared_ptr<PackageConf> &proj, std::shared_ptr<Log> log = nullptr); bool create_project_tarball(std::shared_ptr<PackageConf> &proj, std::shared_ptr<Log> log = NULL);
bool create_project_tarball(std::shared_ptr<PackageConf> &proj, std::shared_ptr<Log> log = nullptr); std::tuple<bool, std::set<std::string>> build_project(std::shared_ptr<PackageConf> proj, std::shared_ptr<Log> log = NULL);
std::tuple<bool, std::set<std::string>> build_project(std::shared_ptr<PackageConf> proj, std::shared_ptr<Log> log = nullptr); bool upload_and_lint(std::shared_ptr<PackageConf> &proj, const std::set<std::string> changes_files, bool skip_dput, std::shared_ptr<Log> log = NULL);
bool upload_and_lint(std::shared_ptr<PackageConf> &proj,
const std::set<std::string> changes_files,
bool skip_dput,
std::shared_ptr<Log> log = nullptr);
// Summary & cleanup // Perform cleanup and summarize the build process
void do_summary(bool skip_cleanup); void do_summary(bool skip_cleanup);
// Orchestrate entire pipeline // Process the entire pipeline for a given PackageConf ID
void process_entire_pipeline(std::shared_ptr<PackageConf> &proj, void process_entire_pipeline(std::shared_ptr<PackageConf> &proj, bool skip_dput, bool skip_cleanup);
bool skip_dput,
bool skip_cleanup);
// Retrieve PackageConf entries (with optional pagination/sorting) // Retrieve all PackageConf entries from the database
std::vector<std::shared_ptr<PackageConf>> get_config(const std::string &repo_name = "", std::vector<std::shared_ptr<PackageConf>> get_config(const std::string &repo_name = "", int page = 0, int per_page = 0, const std::string& sort_by = "", const std::string& sort_order = "");
int page = 0,
int per_page = 0,
const std::string &sort_by = "",
const std::string &sort_order = "");
// Enqueue a task (wrapper) // Function to enqueue tasks
void enqueue(std::function<void()> task); void enqueue(std::function<void()> task);
// Job status and PackageConf getters std::shared_ptr<std::map<std::string, std::shared_ptr<JobStatus>>> get_job_statuses();
std::shared_ptr<std::map<std::string, std::shared_ptr<JobStatus>>> get_job_statuses(); std::vector<std::shared_ptr<PackageConf>> get_packageconfs();
std::vector<std::shared_ptr<PackageConf>> get_packageconfs(); std::shared_ptr<PackageConf> get_packageconf_by_id(int id);
std::shared_ptr<PackageConf> get_packageconf_by_id(int id); std::vector<std::shared_ptr<PackageConf>> get_packageconfs_by_ids(std::set<int> ids);
std::vector<std::shared_ptr<PackageConf>> get_packageconfs_by_ids(std::set<int> ids); void set_packageconfs(std::vector<std::shared_ptr<PackageConf>> _pkgconfs);
void set_packageconfs(std::vector<std::shared_ptr<PackageConf>> _pkgconfs); void sync(std::shared_ptr<PackageConf> pkgconf);
void sync(std::shared_ptr<PackageConf> pkgconf);
// Queue tasks std::string queue_pull_tarball(std::vector<std::shared_ptr<PackageConf>> repos,
std::string queue_pull_tarball(std::vector<std::shared_ptr<PackageConf>> repos, std::unique_ptr<TaskQueue>& task_queue,
std::unique_ptr<TaskQueue>& task_queue, std::shared_ptr<std::map<std::string, std::shared_ptr<JobStatus>>> job_statuses);
std::shared_ptr<std::map<std::string, std::shared_ptr<JobStatus>>> job_statuses); std::string queue_build_upload(std::vector<std::shared_ptr<PackageConf>> repos,
std::string queue_build_upload(std::vector<std::shared_ptr<PackageConf>> repos, std::unique_ptr<TaskQueue>& task_queue,
std::unique_ptr<TaskQueue>& task_queue, std::shared_ptr<std::map<std::string, std::shared_ptr<JobStatus>>> job_statuses);
std::shared_ptr<std::map<std::string, std::shared_ptr<JobStatus>>> job_statuses);
// Get a tasks log std::string get_task_log(int task_id);
std::string get_task_log(int task_id);
std::vector<std::shared_ptr<PackageConf>> list_known_repos(int page = 0, std::vector<Release> releases;
int per_page = 0, std::vector<Package> packages;
const std::string &sort_by = "", std::vector<Branch> branches;
const std::string &sort_order = "");
bool pull_repo_by_name(const std::string &repo_name, std::shared_ptr<Log> log = nullptr);
bool create_project_tarball_by_name(const std::string &repo_name, std::shared_ptr<Log> log = nullptr);
bool build_repo_by_name(const std::string &repo_name, std::shared_ptr<Log> log = nullptr);
// These come from the config/DB private:
std::vector<Release> releases; void debuild_package(const fs::path &packaging_dir, std::shared_ptr<Log> log);
std::vector<Package> packages;
std::vector<Branch> branches;
private: QSqlDatabase p_db;
void debuild_package(const fs::path &packaging_dir, std::shared_ptr<Log> log);
QSqlDatabase p_db; mutable std::mutex packageconfs_mutex_;
mutable std::mutex packageconfs_mutex_; std::vector<std::shared_ptr<PackageConf>> packageconfs;
std::vector<std::shared_ptr<PackageConf>> packageconfs; std::shared_ptr<std::map<std::string, std::shared_ptr<JobStatus>>> _cached_job_statuses;
std::shared_ptr<std::map<std::string, std::shared_ptr<JobStatus>>> _cached_job_statuses;
struct package_conf_item { struct package_conf_item {
std::shared_ptr<PackageConf> first_pkgconf; std::shared_ptr<PackageConf> first_pkgconf;
std::shared_ptr<Task> first_pull_task = std::make_shared<Task>(); std::shared_ptr<Task> first_pull_task = std::make_shared<Task>();
std::shared_ptr<Task> first_tarball_task = std::make_shared<Task>(); std::shared_ptr<Task> first_tarball_task = std::make_shared<Task>();
std::shared_ptr<GitCommit> packaging_commit = std::make_shared<GitCommit>(); std::shared_ptr<GitCommit> packaging_commit = std::make_shared<GitCommit>();
std::shared_ptr<GitCommit> upstream_commit = std::make_shared<GitCommit>(); std::shared_ptr<GitCommit> upstream_commit = std::make_shared<GitCommit>();
}; };
}; };
#endif // CI_LOGIC_H #endif // CI_LOGIC_H

339
cpp/common.cpp Normal file
View File

@ -0,0 +1,339 @@
// Copyright (C) 2024-2025 Simon Quigley <tsimonq2@ubuntu.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "common.h"
#include "utilities.h"
#include "/usr/include/archive.h"
#include <archive_entry.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <cstdio>
#include <cstdlib>
#include <regex>
#include <chrono>
#include <ctime>
#include <mutex>
#include <unordered_set>
#include <QProcess>
// Define the global 'verbose' variable
bool verbose = false;
// Logger function implementations
void log_info(const std::string &msg) {
std::cout << "[INFO] " << msg << "\n";
}
void log_warning(const std::string &msg) {
std::cerr << "[WARNING] " << msg << "\n";
}
void log_error(const std::string &msg) {
std::cerr << "[ERROR] " << msg << "\n";
}
void log_verbose(const std::string &msg) {
if (verbose) {
std::cout << "[VERBOSE] " << msg << "\n";
}
}
namespace fs = std::filesystem;
bool run_command(const std::vector<std::string> &cmd,
const std::optional<std::filesystem::path> &cwd,
bool show_output,
std::shared_ptr<Log> log) {
if (cmd.empty()) {
throw std::runtime_error("Command is empty");
}
QProcess process;
// Set the working directory if provided
if (cwd) {
process.setWorkingDirectory(QString::fromStdString(cwd->string()));
}
// Set up the environment (if needed)
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
process.setProcessEnvironment(env);
// Extract executable and arguments
QString program = QString::fromStdString(cmd[0]);
QStringList arguments;
for (size_t i = 1; i < cmd.size(); ++i) {
arguments << QString::fromStdString(cmd[i]);
}
// Start the command
process.start(program, arguments);
if (!process.waitForStarted()) {
throw std::runtime_error("Failed to start the command: " + program.toStdString());
}
// Stream output while the process is running
while (process.state() == QProcess::Running) {
if (process.waitForReadyRead()) {
QByteArray output = process.readAllStandardOutput();
QByteArray error = process.readAllStandardError();
if (log) {
log->append(output.toStdString());
log->append(error.toStdString());
}
if (show_output) {
std::cout << output.toStdString();
std::cerr << error.toStdString();
}
}
}
// Wait for the process to finish
process.waitForFinished();
// Capture return code and errors
if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
QByteArray error_output = process.readAllStandardError();
std::string error_message = "Command failed with exit code: " + std::to_string(process.exitCode());
if (!error_output.isEmpty()) {
error_message += "\nError Output: " + error_output.toStdString();
}
throw std::runtime_error(error_message);
}
return true;
}
// Function to extract excluded files from a copyright file
std::vector<std::string> extract_files_excluded(const std::string& filepath) {
std::ifstream file(filepath);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file: " + filepath);
}
std::vector<std::string> files_excluded;
std::string line;
std::regex files_excluded_pattern(R"(Files-Excluded:\s*(.*))");
bool in_files_excluded = false;
while (std::getline(file, line)) {
if (std::regex_match(line, files_excluded_pattern)) {
in_files_excluded = true;
std::smatch match;
if (std::regex_search(line, match, files_excluded_pattern) && match.size() > 1) {
files_excluded.emplace_back(match[1]);
}
} else if (in_files_excluded) {
if (!line.empty() && (line[0] == ' ' || line[0] == '\t')) {
files_excluded.emplace_back(line.substr(1));
} else {
break; // End of Files-Excluded block
}
}
}
return files_excluded;
}
// Function to create a tarball
void create_tarball(const std::string& tarballPath, const std::string& directory, const std::vector<std::string>& exclusions, std::shared_ptr<Log> log) {
log->append("Creating tarball: " + tarballPath);
struct archive* a = archive_write_new();
if (!a) {
throw std::runtime_error("Failed to create a new archive.");
}
if (archive_write_add_filter_gzip(a) != ARCHIVE_OK) {
std::string err = "Failed to add gzip filter: ";
err += archive_error_string(a);
archive_write_free(a);
throw std::runtime_error(err);
}
if (archive_write_set_format_pax_restricted(a) != ARCHIVE_OK) {
std::string err = "Failed to set format: ";
err += archive_error_string(a);
archive_write_free(a);
throw std::runtime_error(err);
}
if (archive_write_open_filename(a, tarballPath.c_str()) != ARCHIVE_OK) {
std::string err = "Could not open tarball for writing: ";
err += archive_error_string(a);
archive_write_free(a);
throw std::runtime_error(err);
}
// Initialize a set to track added relative paths to prevent duplication
std::unordered_set<std::string> added_paths;
// Iterate through the directory recursively without following symlinks
for (auto it = fs::recursive_directory_iterator(
directory,
fs::directory_options::skip_permission_denied);
it != fs::recursive_directory_iterator(); ++it) {
const auto& path = it->path();
std::error_code ec;
fs::path relative_path = fs::relative(path, directory, ec);
if (ec) {
log->append("Failed to compute relative path for: " + path.string() + " Error: " + ec.message());
continue;
}
// Normalize the relative path to avoid discrepancies
fs::path normalized_relative_path = relative_path.lexically_normal();
std::string relative_path_str = normalized_relative_path.string();
// Check if this path has already been added
if (!added_paths.insert(relative_path_str).second) {
log->append("Duplicate path detected and skipped: " + relative_path_str);
continue; // Skip adding this duplicate path
}
// Exclusion logic (if any exclusions are provided)
bool excluded = std::any_of(exclusions.begin(), exclusions.end(), [&relative_path_str](const std::string& exclusion) {
return relative_path_str.find(exclusion) != std::string::npos;
});
if (excluded) { continue; }
fs::file_status fstatus = it->symlink_status(ec);
if (ec) {
log->append("Failed to get file status for: " + path.string() + " Error: " + ec.message());
continue;
}
struct archive_entry* entry = archive_entry_new();
if (!entry) {
log->append("Failed to create archive entry for: " + path.string());
archive_write_free(a);
throw std::runtime_error("Failed to create archive entry.");
}
std::string entry_path = relative_path_str;
if (fs::is_directory(fstatus)) {
// Ensure the directory pathname ends with '/'
if (!entry_path.empty() && entry_path.back() != '/') {
entry_path += '/';
}
archive_entry_set_pathname(entry, entry_path.c_str());
} else {
archive_entry_set_pathname(entry, entry_path.c_str());
}
// Set file type, permissions, and size
if (fs::is_regular_file(fstatus)) {
// Regular file
uintmax_t filesize = fs::file_size(path, ec);
if (ec) {
log->append("Cannot get file size for: " + path.string() + " Error: " + ec.message());
archive_entry_free(entry);
continue;
}
archive_entry_set_size(entry, static_cast<off_t>(filesize));
archive_entry_set_filetype(entry, AE_IFREG);
archive_entry_set_perm(entry, static_cast<mode_t>(fstatus.permissions()));
}
else if (fs::is_symlink(fstatus)) {
fs::path target = fs::read_symlink(path, ec);
if (ec) {
log->append("Cannot read symlink for: " + path.string() + " Error: " + ec.message());
archive_entry_free(entry);
continue;
}
archive_entry_set_symlink(entry, target.c_str());
archive_entry_set_filetype(entry, AE_IFLNK);
archive_entry_set_perm(entry, static_cast<mode_t>(fstatus.permissions()));
}
else if (fs::is_directory(fstatus)) {
archive_entry_set_size(entry, 0);
archive_entry_set_filetype(entry, AE_IFDIR);
archive_entry_set_perm(entry, static_cast<mode_t>(fstatus.permissions()));
}
else {
log->append("Unsupported file type for: " + path.string());
archive_entry_free(entry);
continue;
}
// Retrieve and set the modification time
fs::file_time_type ftime = fs::last_write_time(path, ec);
std::time_t mtime;
if (ec) {
log->append("Failed to get last write time for: " + path.string() + " Error: " + ec.message());
// Obtain current UTC time as fallback
auto now = std::chrono::system_clock::now();
mtime = std::chrono::system_clock::to_time_t(now);
log->append("Setting default mtime (current UTC time) for: " + path.string());
} else {
mtime = to_time_t(ftime);
}
archive_entry_set_mtime(entry, mtime, 0);
if (archive_write_header(a, entry) != ARCHIVE_OK) {
log->append("Failed to write header for: " + path.string() + " Error: " + archive_error_string(a));
archive_entry_free(entry);
continue;
}
if (fs::is_regular_file(fstatus)) {
std::ifstream fileStream(path, std::ios::binary);
if (!fileStream) {
log->append("Failed to open file for reading: " + path.string());
archive_entry_free(entry);
continue;
}
const std::size_t bufferSize = 8192;
char buffer[bufferSize];
while (fileStream) {
fileStream.read(buffer, bufferSize);
std::streamsize bytesRead = fileStream.gcount();
if (bytesRead > 0) {
if (archive_write_data(a, buffer, static_cast<size_t>(bytesRead)) < 0) {
log->append("Failed to write data for: " + path.string() + " Error: " + archive_error_string(a));
break;
}
}
}
if (fileStream.bad()) {
log->append("Error reading file: " + path.string());
}
}
archive_entry_free(entry);
}
if (archive_write_close(a) != ARCHIVE_OK) {
std::string err = "Failed to close archive: ";
err += archive_error_string(a);
archive_write_free(a);
throw std::runtime_error(err);
}
if (archive_write_free(a) != ARCHIVE_OK) {
std::string err = "Failed to free archive: ";
err += archive_error_string(a);
throw std::runtime_error(err);
}
log->append("Tarball created and compressed: " + tarballPath);
}

89
cpp/common.h Normal file
View File

@ -0,0 +1,89 @@
// Copyright (C) 2024 Simon Quigley <tsimonq2@ubuntu.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef COMMON_H
#define COMMON_H
#include "utilities.h"
#include <string>
#include <optional>
#include <filesystem>
#include <shared_mutex>
#include <mutex>
#include <vector>
#include <regex>
namespace fs = std::filesystem;
class Task;
class Log {
private:
std::string data = "";
mutable std::shared_mutex lock_;
std::weak_ptr<Task> task_context_;
std::string last_data_str = "";
public:
void append(const std::string& str) {
std::unique_lock lock(lock_);
std::string log_str = str.ends_with('\n') ? str : str + '\n';
if (str.empty() || last_data_str == log_str) { return; }
else if (str.contains("dpkg-source: warning: ignoring deletion of file")) { return; }
data += std::format("[{}] {}", get_current_utc_time("%Y-%m-%dT%H:%M:%SZ"), log_str);
last_data_str = log_str;
}
void set_log(const std::string& str) {
std::unique_lock lock(lock_);
data = str;
}
std::string get() const {
std::shared_lock lock(lock_);
return std::regex_replace(data, std::regex(R"(^\s+)"), "");
}
void assign_task_context(std::shared_ptr<Task> task) {
task_context_ = task;
}
std::shared_ptr<Task> get_task_context() const {
return task_context_.lock();
}
};
// Logger functions
extern bool verbose;
void log_info(const std::string &msg);
void log_warning(const std::string &msg);
void log_error(const std::string &msg);
void log_verbose(const std::string &msg);
// Function to run a command with optional working directory and show output
bool run_command(const std::vector<std::string> &cmd,
const std::optional<fs::path> &cwd = std::nullopt,
bool show_output = false,
std::shared_ptr<Log> log = nullptr);
// Function to extract excluded files from a copyright file
std::vector<std::string> extract_files_excluded(const std::string& filepath);
// Function to create a tarball
void create_tarball(const std::string& tarballPath,
const std::string& directory,
const std::vector<std::string>& exclusions,
std::shared_ptr<Log> log = nullptr);
#endif // COMMON_H

View File

@ -1,4 +1,4 @@
#include "utilities.h" #include "common.h"
#include "ci_logic.h" #include "ci_logic.h"
#include <yaml-cpp/yaml.h> #include <yaml-cpp/yaml.h>
#include <filesystem> #include <filesystem>

82
cpp/lubuntuci_lib.cpp Normal file
View File

@ -0,0 +1,82 @@
// Copyright (C) 2024-2025 Simon Quigley <tsimonq2@ubuntu.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "lubuntuci_lib.h"
#include "ci_logic.h"
#include "common.h"
#include <yaml-cpp/yaml.h>
#include <filesystem>
#include <iostream>
#include <vector>
#include <string>
#include <mutex>
#include <git2.h>
namespace fs = std::filesystem;
/**
* list_known_repos():
* Make sure we call CiLogic::init_global() before reading
* the config, otherwise the config node will be empty.
*/
std::vector<std::shared_ptr<PackageConf>> LubuntuCI::list_known_repos(int page, int per_page, const std::string& sort_by, const std::string& sort_order)
{
cilogic.init_global();
if (page == 0 || per_page == 0 || sort_by.empty() || sort_order.empty()) { return cilogic.get_config(); }
return cilogic.get_config("", page, per_page, sort_by, sort_order);
}
/**
* pull_repo():
* - We do not call init_global() here because list_known_repos()
* or build_repo() might do it. But calling it again is safe.
*/
bool LubuntuCI::pull_repo(const std::string &repo_name, std::shared_ptr<Log> log)
{
log->append("Ensuring the global config is initialized...\n");
cilogic.init_global();
log->append("Global config is initialized. Getting the configs for the package name...\n");
auto pkgconfs = cilogic.get_config(repo_name);
log->append("Configs retrieved. Performing the pull...\n");
return cilogic.pull_project(pkgconfs.at(0), log);
}
/**
* create_project_tarball
*/
bool LubuntuCI::create_project_tarball(const std::string &repo_name, std::shared_ptr<Log> log)
{
cilogic.init_global();
log->append("Global config is initialized. Getting the configs for the package name...\n");
auto pkgconfs = cilogic.get_config(repo_name);
log->append("Configs retrieved. Performing the tarball creation...\n");
return cilogic.create_project_tarball(pkgconfs.at(0), log);
}
/**
* build_repo():
* - Also safely calls init_global().
* - Reads skip_dput from config if present (default = false).
*/
bool LubuntuCI::build_repo(const std::string &repo_name, std::shared_ptr<Log> log)
{
cilogic.init_global();
bool success = true;
for (auto pkgconf : cilogic.get_config(repo_name)) {
const auto [build_success, changes_files] = cilogic.build_project(pkgconf, log);
success = success && build_success && cilogic.upload_and_lint(pkgconf, changes_files, false);
}
return success;
}

48
cpp/lubuntuci_lib.h Normal file
View File

@ -0,0 +1,48 @@
// Copyright (C) 2024-2025 Simon Quigley <tsimonq2@ubuntu.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef LUBUNTUCI_LIB_H
#define LUBUNTUCI_LIB_H
#include <string>
#include <vector>
#include "ci_logic.h"
class LubuntuCI {
public:
/**
* List all known repositories from the merged config.
*/
std::vector<std::shared_ptr<PackageConf>> list_known_repos(int page = 0,
int per_page = 0,
const std::string& sort_by = "",
const std::string& sort_order = "");
/**
* Pull a specific repository by name (returns true on success).
*/
bool pull_repo(const std::string &repo_name, std::shared_ptr<Log> log = NULL);
bool create_project_tarball(const std::string &repo_name, std::shared_ptr<Log> log);
/**
* Build a specific repository by name (returns true on success).
*/
bool build_repo(const std::string &repo_name, std::shared_ptr<Log> log = NULL);
CiLogic cilogic = CiLogic();
};
#endif // LUBUNTUCI_LIB_H

View File

@ -33,16 +33,22 @@ void TaskQueue::enqueue(std::shared_ptr<JobStatus> jobstatus,
auto now = std::chrono::duration_cast<std::chrono::milliseconds>( auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()) std::chrono::system_clock::now().time_since_epoch())
.count(); .count();
// Create the task
std::shared_ptr<Task> task_ptr = std::make_shared<Task>(jobstatus, now, packageconf); std::shared_ptr<Task> task_ptr = std::make_shared<Task>(jobstatus, now, packageconf);
task_ptr->func = [task_func, self_weak = std::weak_ptr<Task>(task_ptr)](std::shared_ptr<Log> log) mutable { task_ptr->func = [task_func, self_weak = std::weak_ptr<Task>(task_ptr)](std::shared_ptr<Log> log) {
if (auto task_locked = self_weak.lock()) std::shared_ptr<Task> task_locked = self_weak.lock();
if (task_locked) {
log->assign_task_context(task_locked);
task_func(log); task_func(log);
}
}; };
packageconf->assign_task(jobstatus, task_ptr, packageconf); packageconf->assign_task(jobstatus, task_ptr, packageconf);
std::unique_lock<std::mutex> lock(tasks_mutex_); std::unique_lock<std::mutex> lock(tasks_mutex_);
tasks_.emplace(task_ptr); tasks_.emplace(task_ptr);
} }
cv_.notify_all(); cv_.notify_all(); // Notify worker threads
} }
void TaskQueue::start() { void TaskQueue::start() {
@ -53,12 +59,14 @@ void TaskQueue::start() {
} }
void TaskQueue::stop() { void TaskQueue::stop() {
{ {
std::unique_lock<std::mutex> lock(tasks_mutex_); std::unique_lock<std::mutex> tasks_lock(tasks_mutex_);
std::unique_lock<std::mutex> packages_lock(running_packages_mutex_);
std::unique_lock<std::mutex> running_tasks_lock(running_tasks_mutex_);
stop_ = true; stop_ = true;
} }
cv_.notify_all(); cv_.notify_all(); // Wake up all threads
for (auto &worker : workers_) { for (auto& worker : workers_) {
if (worker.joinable()) { if (worker.joinable()) {
worker.join(); worker.join();
} }
@ -80,42 +88,63 @@ void TaskQueue::worker_thread() {
while (true) { while (true) {
std::shared_ptr<Task> task_to_execute; std::shared_ptr<Task> task_to_execute;
{ {
std::unique_lock<std::mutex> lock(tasks_mutex_); std::lock_guard<std::mutex> tasks_lock(tasks_mutex_);
cv_.wait(lock, [this] { return stop_ || !tasks_.empty(); });
if (stop_ && tasks_.empty()) return; if (stop_ && tasks_.empty()) return;
auto it = tasks_.begin(); auto it = tasks_.begin();
bool found_valid = false;
// Iterate through the set until a valid task is found
while (it != tasks_.end()) { while (it != tasks_.end()) {
int package_id = (*it)->get_parent_packageconf()->package->id;
{ {
std::lock_guard<std::mutex> pkg_lock(running_packages_mutex_); std::shared_ptr<Task> it_task = *it;
auto running_it = std::find_if(running_packages_.begin(), running_packages_.end(), task_to_execute = it_task;
[package_id](const std::shared_ptr<Package> &pkg) { return pkg->id == package_id; }); }
if (running_it != running_packages_.end()) {
++it; int package_id = task_to_execute->get_parent_packageconf()->package->id;
{
std::lock_guard<std::mutex> lock(running_packages_mutex_);
auto running_package_it = std::find_if(running_packages_.begin(), running_packages_.end(),
[&package_id](const std::shared_ptr<Package>& package) { return package->id == package_id; });
if (running_package_it != running_packages_.end()) {
++it; // Move to the next task
continue; continue;
} }
} }
task_to_execute = *it;
tasks_.erase(it); // Task is valid to execute
found_valid = true;
it = tasks_.erase(it);
break; break;
} }
if (!found_valid) { continue; }
} }
if (!task_to_execute || !task_to_execute->func) continue; if (!task_to_execute || !task_to_execute->func) continue;
{ else {
std::lock_guard<std::mutex> pkg_lock(running_packages_mutex_); {
running_packages_.insert(task_to_execute->get_parent_packageconf()->package); std::lock_guard<std::mutex> packages_lock(running_packages_mutex_);
running_packages_.insert(task_to_execute->get_parent_packageconf()->package);
}
{
std::lock_guard<std::mutex> tasks_lock(running_tasks_mutex_);
running_tasks_.insert(task_to_execute);
}
} }
// Set the start time
{ {
std::lock_guard<std::mutex> rt_lock(running_tasks_mutex_); auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
running_tasks_.insert(task_to_execute); std::chrono::system_clock::now().time_since_epoch())
.count();
task_to_execute->start_time = now;
} }
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
task_to_execute->start_time = now;
try { try {
task_to_execute->func(task_to_execute->log); task_to_execute->func(task_to_execute->log); // Execute the task
task_to_execute->successful = true; task_to_execute->successful = true;
} catch (const std::exception &e) { } catch (const std::exception& e) {
task_to_execute->successful = false; task_to_execute->successful = false;
std::ostringstream oss; std::ostringstream oss;
oss << "Exception type: " << typeid(e).name() << "\n" oss << "Exception type: " << typeid(e).name() << "\n"
@ -125,27 +154,39 @@ void TaskQueue::worker_thread() {
task_to_execute->successful = false; task_to_execute->successful = false;
task_to_execute->log->append("Unknown exception occurred"); task_to_execute->log->append("Unknown exception occurred");
} }
now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
task_to_execute->finish_time = now;
{ {
std::lock_guard<std::mutex> rt_lock(running_tasks_mutex_); auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
task_to_execute->finish_time = now;
}
{
// Remove the task from running_tasks_
std::lock_guard<std::mutex> lock(running_tasks_mutex_);
int id = task_to_execute->id; int id = task_to_execute->id;
auto it = std::find_if(running_tasks_.begin(), running_tasks_.end(), auto running_task_it = std::find_if(running_tasks_.begin(), running_tasks_.end(),
[id](const std::shared_ptr<Task> &task) { return task->id == id; }); [&id](const std::shared_ptr<Task>& task) { return task->id == id; });
if (it != running_tasks_.end()) {
running_tasks_.erase(it); if (running_task_it != running_tasks_.end()) {
running_tasks_.erase(running_task_it);
} }
} }
{ {
std::lock_guard<std::mutex> pkg_lock(running_packages_mutex_); // Remove packageconf from running_packages_ by id
std::lock_guard<std::mutex> lock(running_packages_mutex_);
int package_id = task_to_execute->get_parent_packageconf()->package->id; int package_id = task_to_execute->get_parent_packageconf()->package->id;
auto it = std::find_if(running_packages_.begin(), running_packages_.end(), auto running_package_it = std::find_if(running_packages_.begin(), running_packages_.end(),
[package_id](const std::shared_ptr<Package> &pkg) { return pkg->id == package_id; }); [&package_id](const std::shared_ptr<Package>& package) { return package->id == package_id; });
if (it != running_packages_.end()) {
running_packages_.erase(it); if (running_package_it != running_packages_.end()) {
running_packages_.erase(running_package_it);
} }
} }
// Save to the database at the end
task_to_execute->save(0); task_to_execute->save(0);
} }
} }

View File

@ -1,4 +1,6 @@
// Copyright (C) 2024-2025 Simon Quigley <tsimonq2@ubuntu.com> // cpp/update-maintainer.cpp
// Copyright (C) 2024 Simon Quigley <tsimonq2@ubuntu.com>
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
@ -13,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "ci_logic.h" #include "lubuntuci_lib.h"
#include <iostream> #include <iostream>
int main(int argc, char** argv) { int main(int argc, char** argv) {

View File

@ -14,9 +14,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "utilities.h" #include "utilities.h"
#include "common.h"
#include "/usr/include/archive.h"
#include <archive_entry.h>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <filesystem> #include <filesystem>
@ -26,16 +25,17 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <sstream> #include <sstream>
#include <random> #include <random>
#include <queue>
#include <ranges> #include <ranges>
#include <format> #include <format> // for std::format in C++20/23
#include <unordered_set>
bool verbose = false; namespace fs = std::filesystem;
// Define a semaphore with a maximum of 10 concurrent jobs // Define a semaphore with a maximum of 10 concurrent jobs
static std::counting_semaphore<10> sem(10); static std::counting_semaphore<10> sem(10);
// Job queue and synchronization primitives // Job queue and synchronization primitives
static std::queue<std::function<void()>> job_queue;
static std::mutex queue_mutex; static std::mutex queue_mutex;
static std::atomic<bool> daemon_running{false}; static std::atomic<bool> daemon_running{false};
@ -296,308 +296,3 @@ void run_task_every(std::stop_token _stop_token, int interval_minutes, std::func
std::this_thread::sleep_for(std::chrono::minutes(interval_minutes)); std::this_thread::sleep_for(std::chrono::minutes(interval_minutes));
} }
} }
// Logger function implementations
void log_info(const std::string &msg) {
std::cout << "[INFO] " << msg << "\n";
}
void log_warning(const std::string &msg) {
std::cerr << "[WARNING] " << msg << "\n";
}
void log_error(const std::string &msg) {
std::cerr << "[ERROR] " << msg << "\n";
}
void log_verbose(const std::string &msg) {
if (verbose) {
std::cout << "[VERBOSE] " << msg << "\n";
}
}
namespace fs = std::filesystem;
bool run_command(const std::vector<std::string> &cmd,
const std::optional<std::filesystem::path> &cwd,
bool show_output,
std::shared_ptr<Log> log) {
if (cmd.empty()) {
throw std::runtime_error("Command is empty");
}
QProcess process;
// Set the working directory if provided
if (cwd) {
process.setWorkingDirectory(QString::fromStdString(cwd->string()));
}
// Set up the environment (if needed)
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
process.setProcessEnvironment(env);
// Extract executable and arguments
QString program = QString::fromStdString(cmd[0]);
QStringList arguments;
for (size_t i = 1; i < cmd.size(); ++i) {
arguments << QString::fromStdString(cmd[i]);
}
// Start the command
process.start(program, arguments);
if (!process.waitForStarted()) {
throw std::runtime_error("Failed to start the command: " + program.toStdString());
}
// Stream output while the process is running
while (process.state() == QProcess::Running) {
if (process.waitForReadyRead()) {
QByteArray output = process.readAllStandardOutput();
QByteArray error = process.readAllStandardError();
if (log) {
log->append(output.toStdString());
log->append(error.toStdString());
}
if (show_output) {
std::cout << output.toStdString();
std::cerr << error.toStdString();
}
}
}
// Wait for the process to finish
process.waitForFinished();
// Capture return code and errors
if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
QByteArray error_output = process.readAllStandardError();
std::string error_message = "Command failed with exit code: " + std::to_string(process.exitCode());
if (!error_output.isEmpty()) {
error_message += "\nError Output: " + error_output.toStdString();
}
throw std::runtime_error(error_message);
}
return true;
}
// Function to extract excluded files from a copyright file
std::vector<std::string> extract_files_excluded(const std::string& filepath) {
std::ifstream file(filepath);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file: " + filepath);
}
std::vector<std::string> files_excluded;
std::string line;
std::regex files_excluded_pattern(R"(Files-Excluded:\s*(.*))");
bool in_files_excluded = false;
while (std::getline(file, line)) {
if (std::regex_match(line, files_excluded_pattern)) {
in_files_excluded = true;
std::smatch match;
if (std::regex_search(line, match, files_excluded_pattern) && match.size() > 1) {
files_excluded.emplace_back(match[1]);
}
} else if (in_files_excluded) {
if (!line.empty() && (line[0] == ' ' || line[0] == '\t')) {
files_excluded.emplace_back(line.substr(1));
} else {
break; // End of Files-Excluded block
}
}
}
return files_excluded;
}
// Function to create a tarball
void create_tarball(const std::string& tarballPath, const std::string& directory, const std::vector<std::string>& exclusions, std::shared_ptr<Log> log) {
log->append("Creating tarball: " + tarballPath);
struct archive* a = archive_write_new();
if (!a) {
throw std::runtime_error("Failed to create a new archive.");
}
if (archive_write_add_filter_gzip(a) != ARCHIVE_OK) {
std::string err = "Failed to add gzip filter: ";
err += archive_error_string(a);
archive_write_free(a);
throw std::runtime_error(err);
}
if (archive_write_set_format_pax_restricted(a) != ARCHIVE_OK) {
std::string err = "Failed to set format: ";
err += archive_error_string(a);
archive_write_free(a);
throw std::runtime_error(err);
}
if (archive_write_open_filename(a, tarballPath.c_str()) != ARCHIVE_OK) {
std::string err = "Could not open tarball for writing: ";
err += archive_error_string(a);
archive_write_free(a);
throw std::runtime_error(err);
}
// Initialize a set to track added relative paths to prevent duplication
std::unordered_set<std::string> added_paths;
// Iterate through the directory recursively without following symlinks
for (auto it = fs::recursive_directory_iterator(
directory,
fs::directory_options::skip_permission_denied);
it != fs::recursive_directory_iterator(); ++it) {
const auto& path = it->path();
std::error_code ec;
fs::path relative_path = fs::relative(path, directory, ec);
if (ec) {
log->append("Failed to compute relative path for: " + path.string() + " Error: " + ec.message());
continue;
}
// Normalize the relative path to avoid discrepancies
fs::path normalized_relative_path = relative_path.lexically_normal();
std::string relative_path_str = normalized_relative_path.string();
// Check if this path has already been added
if (!added_paths.insert(relative_path_str).second) {
log->append("Duplicate path detected and skipped: " + relative_path_str);
continue; // Skip adding this duplicate path
}
// Exclusion logic (if any exclusions are provided)
bool excluded = std::any_of(exclusions.begin(), exclusions.end(), [&relative_path_str](const std::string& exclusion) {
return relative_path_str.find(exclusion) != std::string::npos;
});
if (excluded) { continue; }
fs::file_status fstatus = it->symlink_status(ec);
if (ec) {
log->append("Failed to get file status for: " + path.string() + " Error: " + ec.message());
continue;
}
struct archive_entry* entry = archive_entry_new();
if (!entry) {
log->append("Failed to create archive entry for: " + path.string());
archive_write_free(a);
throw std::runtime_error("Failed to create archive entry.");
}
std::string entry_path = relative_path_str;
if (fs::is_directory(fstatus)) {
// Ensure the directory pathname ends with '/'
if (!entry_path.empty() && entry_path.back() != '/') {
entry_path += '/';
}
archive_entry_set_pathname(entry, entry_path.c_str());
} else {
archive_entry_set_pathname(entry, entry_path.c_str());
}
// Set file type, permissions, and size
if (fs::is_regular_file(fstatus)) {
// Regular file
uintmax_t filesize = fs::file_size(path, ec);
if (ec) {
log->append("Cannot get file size for: " + path.string() + " Error: " + ec.message());
archive_entry_free(entry);
continue;
}
archive_entry_set_size(entry, static_cast<off_t>(filesize));
archive_entry_set_filetype(entry, AE_IFREG);
archive_entry_set_perm(entry, static_cast<mode_t>(fstatus.permissions()));
}
else if (fs::is_symlink(fstatus)) {
fs::path target = fs::read_symlink(path, ec);
if (ec) {
log->append("Cannot read symlink for: " + path.string() + " Error: " + ec.message());
archive_entry_free(entry);
continue;
}
archive_entry_set_symlink(entry, target.c_str());
archive_entry_set_filetype(entry, AE_IFLNK);
archive_entry_set_perm(entry, static_cast<mode_t>(fstatus.permissions()));
}
else if (fs::is_directory(fstatus)) {
archive_entry_set_size(entry, 0);
archive_entry_set_filetype(entry, AE_IFDIR);
archive_entry_set_perm(entry, static_cast<mode_t>(fstatus.permissions()));
}
else {
log->append("Unsupported file type for: " + path.string());
archive_entry_free(entry);
continue;
}
// Retrieve and set the modification time
fs::file_time_type ftime = fs::last_write_time(path, ec);
std::time_t mtime;
if (ec) {
log->append("Failed to get last write time for: " + path.string() + " Error: " + ec.message());
// Obtain current UTC time as fallback
auto now = std::chrono::system_clock::now();
mtime = std::chrono::system_clock::to_time_t(now);
log->append("Setting default mtime (current UTC time) for: " + path.string());
} else {
mtime = to_time_t(ftime);
}
archive_entry_set_mtime(entry, mtime, 0);
if (archive_write_header(a, entry) != ARCHIVE_OK) {
log->append("Failed to write header for: " + path.string() + " Error: " + archive_error_string(a));
archive_entry_free(entry);
continue;
}
if (fs::is_regular_file(fstatus)) {
std::ifstream fileStream(path, std::ios::binary);
if (!fileStream) {
log->append("Failed to open file for reading: " + path.string());
archive_entry_free(entry);
continue;
}
const std::size_t bufferSize = 8192;
char buffer[bufferSize];
while (fileStream) {
fileStream.read(buffer, bufferSize);
std::streamsize bytesRead = fileStream.gcount();
if (bytesRead > 0) {
if (archive_write_data(a, buffer, static_cast<size_t>(bytesRead)) < 0) {
log->append("Failed to write data for: " + path.string() + " Error: " + archive_error_string(a));
break;
}
}
}
if (fileStream.bad()) {
log->append("Error reading file: " + path.string());
}
}
archive_entry_free(entry);
}
if (archive_write_close(a) != ARCHIVE_OK) {
std::string err = "Failed to close archive: ";
err += archive_error_string(a);
archive_write_free(a);
throw std::runtime_error(err);
}
if (archive_write_free(a) != ARCHIVE_OK) {
std::string err = "Failed to free archive: ";
err += archive_error_string(a);
throw std::runtime_error(err);
}
log->append("Tarball created and compressed: " + tarballPath);
}

View File

@ -18,57 +18,11 @@
#include <string> #include <string>
#include <filesystem> #include <filesystem>
#include <mutex> #include <mutex>
#include <regex>
#include <future> #include <future>
#include <shared_mutex>
#include <semaphore> #include <semaphore>
#include <functional> #include <functional>
#include <git2.h> #include <git2.h>
#include <QProcess>
namespace fs = std::filesystem;
class Task;
// Time utilities
std::string get_current_utc_time(const std::string& format);
std::time_t to_time_t(const std::filesystem::file_time_type& ftime);
class Log {
private:
std::string data = "";
mutable std::shared_mutex lock_;
std::weak_ptr<Task> task_context_;
std::string last_data_str = "";
public:
void append(const std::string& str) {
std::unique_lock lock(lock_);
std::string log_str = str.ends_with('\n') ? str : str + '\n';
if (str.empty() || last_data_str == log_str) { return; }
else if (str.contains("dpkg-source: warning: ignoring deletion of file")) { return; }
data += std::format("[{}] {}", get_current_utc_time("%Y-%m-%dT%H:%M:%SZ"), log_str);
last_data_str = log_str;
}
void set_log(const std::string& str) {
std::unique_lock lock(lock_);
data = str;
}
std::string get() const {
std::shared_lock lock(lock_);
return std::regex_replace(data, std::regex(R"(^\s+)"), "");
}
void assign_task_context(std::shared_ptr<Task> task) {
task_context_ = task;
}
std::shared_ptr<Task> get_task_context() const {
return task_context_.lock();
}
};
// Function to read the entire content of a file into a string // Function to read the entire content of a file into a string
std::string read_file(const std::filesystem::path& filePath); std::string read_file(const std::filesystem::path& filePath);
@ -95,6 +49,10 @@ std::filesystem::path create_temp_directory();
// Function to copy a directory recursively // Function to copy a directory recursively
void copy_directory(const std::filesystem::path& source, const std::filesystem::path& destination); void copy_directory(const std::filesystem::path& source, const std::filesystem::path& destination);
// Time utilities
std::string get_current_utc_time(const std::string& format);
std::time_t to_time_t(const std::filesystem::file_time_type& ftime);
// String utilities // String utilities
std::vector<std::string> split_string(const std::string& input, const std::string& delimiter); std::vector<std::string> split_string(const std::string& input, const std::string& delimiter);
std::string remove_suffix(const std::string& input, const std::string& suffix); std::string remove_suffix(const std::string& input, const std::string& suffix);
@ -107,25 +65,3 @@ std::pair<int, bool> get_version_from_codename(const std::string& codename);
void ensure_git_inited(); void ensure_git_inited();
void run_task_every(std::stop_token _stop_token, int interval_minutes, std::function<void()> task); void run_task_every(std::stop_token _stop_token, int interval_minutes, std::function<void()> task);
// Logger functions
extern bool verbose;
void log_info(const std::string &msg);
void log_warning(const std::string &msg);
void log_error(const std::string &msg);
void log_verbose(const std::string &msg);
// Function to run a command with optional working directory and show output
bool run_command(const std::vector<std::string> &cmd,
const std::optional<fs::path> &cwd = std::nullopt,
bool show_output = false,
std::shared_ptr<Log> log = nullptr);
// Function to extract excluded files from a copyright file
std::vector<std::string> extract_files_excluded(const std::string& filepath);
// Function to create a tarball
void create_tarball(const std::string& tarballPath,
const std::string& directory,
const std::vector<std::string>& exclusions,
std::shared_ptr<Log> log = nullptr);

View File

@ -18,8 +18,6 @@
#include "sources_parser.h" #include "sources_parser.h"
#include "naive_bayes_classifier.h" #include "naive_bayes_classifier.h"
#include "db_common.h" #include "db_common.h"
#include "template_renderer.h"
#include "ci_logic.h"
// Qt includes // Qt includes
#include <QtHttpServer/QHttpServer> #include <QtHttpServer/QHttpServer>
@ -59,6 +57,10 @@
#include "build.h" #include "build.h"
#include "binary_package_publishing_history.h" #include "binary_package_publishing_history.h"
// Local includes
#include "lubuntuci_lib.h"
#include "template_renderer.h"
constexpr QHttpServerResponder::StatusCode StatusCodeFound = QHttpServerResponder::StatusCode::Found; constexpr QHttpServerResponder::StatusCode StatusCodeFound = QHttpServerResponder::StatusCode::Found;
static std::string timestamp_now() static std::string timestamp_now()
@ -129,8 +131,7 @@ WebServer::WebServer(QObject *parent) : QObject(parent) {}
QHttpServerResponse bad_response(StatusCodeFound); QHttpServerResponse bad_response(StatusCodeFound);
QHttpHeaders bad_response_headers; QHttpHeaders bad_response_headers;
bad_response_headers.replaceOrAppend(QHttpHeaders::WellKnownHeader::Location, bad_response_headers.replaceOrAppend(QHttpHeaders::WellKnownHeader::Location, "/unauthorized?base_url=" + base_url + "&redirect_to=" + current_path);
"/unauthorized?base_url=" + base_url + "&redirect_to=" + current_path);
bad_response.setHeaders(bad_response_headers); bad_response.setHeaders(bad_response_headers);
return bad_response; return bad_response;
@ -173,14 +174,14 @@ bool WebServer::start_server(quint16 port) {
} }
archive proposed = proposed_opt.value(); archive proposed = proposed_opt.value();
// Use our new list_known_repos() method from CiLogic std::shared_ptr<PackageConf> _tmp_pkg_conf = std::make_shared<PackageConf>();
std::shared_ptr<CiLogic> cilogic = std::make_shared<CiLogic>(); std::shared_ptr<LubuntuCI> lubuntuci = std::make_shared<LubuntuCI>();
std::vector<std::shared_ptr<PackageConf>> all_repos = cilogic->list_known_repos(); std::vector<std::shared_ptr<PackageConf>> all_repos = lubuntuci->list_known_repos();
task_queue = std::make_unique<TaskQueue>(6); task_queue = std::make_unique<TaskQueue>(6);
std::shared_ptr<std::map<std::string, std::shared_ptr<JobStatus>>> job_statuses = cilogic->get_job_statuses(); std::shared_ptr<std::map<std::string, std::shared_ptr<JobStatus>>> job_statuses = lubuntuci->cilogic.get_job_statuses();
task_queue->start(); task_queue->start();
// Load initial tokens from the database // Load initial tokens
{ {
QSqlQuery load_tokens(get_thread_connection()); QSqlQuery load_tokens(get_thread_connection());
load_tokens.prepare("SELECT person.id, person.username, person.logo_url, person_token.token, person_token.expiry_date FROM person INNER JOIN person_token ON person.id = person_token.person_id"); load_tokens.prepare("SELECT person.id, person.username, person.logo_url, person_token.token, person_token.expiry_date FROM person INNER JOIN person_token ON person.id = person_token.person_id");
@ -198,12 +199,12 @@ bool WebServer::start_server(quint16 port) {
} }
} }
expire_tokens_thread_ = std::jthread(run_task_every, 60, [this, cilogic] { expire_tokens_thread_ = std::jthread(run_task_every, 60, [this, lubuntuci] {
QSqlQuery expired_tokens(get_thread_connection()); QSqlQuery expired_tokens(get_thread_connection());
QString current_time = QDateTime::currentDateTime().toString(Qt::ISODate); QString current_time = QDateTime::currentDateTime().toString(Qt::ISODate);
expired_tokens.prepare("DELETE FROM person_token WHERE expiry_date < :current_time"); expired_tokens.prepare("DELETE FROM person_token WHERE expiry_date < :current_time");
expired_tokens.bindValue(":current_time", current_time); expired_tokens.bindValue(":current_time", QDateTime::currentDateTime().toString(Qt::ISODate));
ci_query_exec(&expired_tokens); ci_query_exec(&expired_tokens);
for (auto it = _active_tokens.begin(); it != _active_tokens.end();) { for (auto it = _active_tokens.begin(); it != _active_tokens.end();) {
if (it.value() <= QDateTime::currentDateTime()) it = _active_tokens.erase(it); if (it.value() <= QDateTime::currentDateTime()) it = _active_tokens.erase(it);
@ -215,9 +216,10 @@ bool WebServer::start_server(quint16 port) {
} }
}); });
process_sources_thread_ = std::jthread(run_task_every, 10, [this, all_repos, proposed, cilogic, job_statuses] { process_sources_thread_ = std::jthread(run_task_every, 10, [this, all_repos, proposed, lubuntuci, job_statuses] {
for (auto pkgconf : all_repos) { for (auto pkgconf : all_repos) {
if (!pkgconf->can_check_source_upload()) continue; if (!pkgconf->can_check_source_upload()) { continue; }
task_queue->enqueue( task_queue->enqueue(
job_statuses->at("source_check"), job_statuses->at("source_check"),
[this, pkgconf, proposed](std::shared_ptr<Log> log) mutable { [this, pkgconf, proposed](std::shared_ptr<Log> log) mutable {
@ -227,17 +229,22 @@ bool WebServer::start_server(quint16 port) {
found_in_ppa = true; found_in_ppa = true;
break; break;
} }
if (!found_in_ppa) throw std::runtime_error("Not found in the PPA.");
if (!found_in_ppa) {
throw std::runtime_error("Not found in the PPA.");
}
}, },
pkgconf pkgconf
); );
pkgconf->sync(); pkgconf->sync();
} }
}); });
process_binaries_thread_ = std::jthread(run_task_every, 15, [this, all_repos, proposed, cilogic, job_statuses] { process_binaries_thread_ = std::jthread(run_task_every, 15, [this, all_repos, proposed, lubuntuci, job_statuses] {
for (auto pkgconf : all_repos) { for (auto pkgconf : all_repos) {
if (!pkgconf->can_check_builds()) continue; if (!pkgconf->can_check_builds()) { continue; }
task_queue->enqueue( task_queue->enqueue(
job_statuses->at("build_check"), job_statuses->at("build_check"),
[this, pkgconf, proposed](std::shared_ptr<Log> log) mutable { [this, pkgconf, proposed](std::shared_ptr<Log> log) mutable {
@ -272,7 +279,7 @@ bool WebServer::start_server(quint16 port) {
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// /unauthorized?base_url=<base_url>&redirect_to=<redirect_to> // /unauthorized?base_url=<base_url>&redirect_to=<redirect_to>
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
http_server_.route("/unauthorized", [this, cilogic](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/unauthorized", [this, lubuntuci](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
// Extract data up front // Extract data up front
auto query = req.query(); auto query = req.query();
QString base_url = query.queryItemValue("base_url"); QString base_url = query.queryItemValue("base_url");
@ -328,7 +335,7 @@ bool WebServer::start_server(quint16 port) {
///////////////// /////////////////
// /authcallback // /authcallback
///////////////// /////////////////
http_server_.route("/authcallback", [this, cilogic](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/authcallback", [this, lubuntuci](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
// Extract data up front // Extract data up front
auto query = req.query(); auto query = req.query();
QString base_url = query.queryItemValue("base_url"); QString base_url = query.queryItemValue("base_url");
@ -469,7 +476,7 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// Route "/" // Route "/"
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/", [this, cilogic, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/", [this, lubuntuci, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
@ -485,13 +492,13 @@ bool WebServer::start_server(quint16 port) {
: query.queryItemValue("sort_order").toStdString(); : query.queryItemValue("sort_order").toStdString();
return QtConcurrent::run([=, this]() { return QtConcurrent::run([=, this]() {
auto all_repos = cilogic->list_known_repos(); auto all_repos = lubuntuci->list_known_repos();
int total_size = static_cast<int>(all_repos.size()); int total_size = static_cast<int>(all_repos.size());
int total_pages = (per_page > 0) int total_pages = (per_page > 0)
? (total_size + per_page - 1) / per_page ? (total_size + per_page - 1) / per_page
: 1; : 1;
auto repos = cilogic->list_known_repos(page, per_page, sort_by, sort_order); auto repos = lubuntuci->list_known_repos(page, per_page, sort_by, sort_order);
if (repos.empty() && total_size == 0) { if (repos.empty() && total_size == 0) {
std::string err_html = R"( std::string err_html = R"(
<html> <html>
@ -512,31 +519,37 @@ bool WebServer::start_server(quint16 port) {
{"total_pages", std::to_string(total_pages)} {"total_pages", std::to_string(total_pages)}
}; };
std::map<std::string, std::vector<std::map<std::string, std::string>>> list_context; std::map<std::string, std::vector<std::map<std::string, std::string>>> list_context;
std::vector<std::map<std::string, std::string>> reposVec; std::vector<std::map<std::string, std::string>> reposVec;
for (const auto &r : repos) { for (const auto &r : repos) {
std::map<std::string, std::string> item; std::map<std::string, std::string> item;
std::string packaging_commit_str; std::string packaging_commit_str;
std::string upstream_commit_str; std::string upstream_commit_str;
if (r->packaging_commit) { if (r->packaging_commit) {
std::string commit_summary = r->packaging_commit->commit_summary; std::string commit_summary = r->packaging_commit->commit_summary;
if (commit_summary.size() > 40) if (commit_summary.size() > 40) {
commit_summary = commit_summary.substr(0, 37) + "..."; commit_summary = commit_summary.substr(0, 37) + "...";
}
packaging_commit_str = r->packaging_commit->commit_hash.substr(0, 7) + packaging_commit_str = r->packaging_commit->commit_hash.substr(0, 7) +
std::format(" ({:%Y-%m-%d %H:%M:%S %Z})<br />", r->packaging_commit->commit_datetime) + std::format(" ({:%Y-%m-%d %H:%M:%S %Z})<br />", r->packaging_commit->commit_datetime) +
commit_summary; commit_summary;
} }
if (r->upstream_commit) { if (r->upstream_commit) {
std::string commit_summary = r->upstream_commit->commit_summary; std::string commit_summary = r->upstream_commit->commit_summary;
if (commit_summary.size() > 40) if (commit_summary.size() > 40) {
commit_summary = commit_summary.substr(0, 37) + "..."; commit_summary = commit_summary.substr(0, 37) + "...";
}
upstream_commit_str = r->upstream_commit->commit_hash.substr(0, 7) + upstream_commit_str = r->upstream_commit->commit_hash.substr(0, 7) +
std::format(" ({:%Y-%m-%d %H:%M:%S %Z})<br />", r->upstream_commit->commit_datetime) + std::format(" ({:%Y-%m-%d %H:%M:%S %Z})<br />", r->upstream_commit->commit_datetime) +
commit_summary; commit_summary;
} }
std::string packaging_commit_url_str = (r->package ? r->package->packaging_browser : "") + std::string packaging_commit_url_str = (r->package ? r->package->packaging_browser : "") +
(r->packaging_commit ? r->packaging_commit->commit_hash : ""); (r->packaging_commit ? r->packaging_commit->commit_hash : "");
std::string upstream_commit_url_str = (r->package ? r->package->upstream_browser : "") + std::string upstream_commit_url_str = (r->package ? r->package->upstream_browser : "") +
(r->upstream_commit ? r->upstream_commit->commit_hash : ""); (r->upstream_commit ? r->upstream_commit->commit_hash : "");
item["id"] = std::to_string(r->id); item["id"] = std::to_string(r->id);
item["name"] = r->package->name; item["name"] = r->package->name;
item["branch_name"] = r->branch->name; item["branch_name"] = r->branch->name;
@ -584,7 +597,7 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /pull?repo=<id> // /pull?repo=<id>
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/pull", [this, cilogic, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/pull", [this, lubuntuci, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
@ -599,7 +612,7 @@ bool WebServer::start_server(quint16 port) {
} }
int repo = std::stoi(repo_string.toStdString()); int repo = std::stoi(repo_string.toStdString());
std::string msg = cilogic->queue_pull_tarball({ cilogic->get_packageconf_by_id(repo) }, task_queue, job_statuses); std::string msg = lubuntuci->cilogic.queue_pull_tarball({ lubuntuci->cilogic.get_packageconf_by_id(repo) }, task_queue, job_statuses);
return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size())); return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size()));
}); });
}); });
@ -607,7 +620,7 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /build?repo=<id> // /build?repo=<id>
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/build", [this, cilogic, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/build", [this, lubuntuci, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
@ -622,7 +635,7 @@ bool WebServer::start_server(quint16 port) {
} }
int repo = std::stoi(repo_string.toStdString()); int repo = std::stoi(repo_string.toStdString());
std::string msg = cilogic->queue_build_upload({ cilogic->get_packageconf_by_id(repo) }, task_queue, job_statuses); std::string msg = lubuntuci->cilogic.queue_build_upload({ lubuntuci->cilogic.get_packageconf_by_id(repo) }, task_queue, job_statuses);
return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size())); return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size()));
}); });
}); });
@ -630,7 +643,7 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /pull-selected?repos=<ids> // /pull-selected?repos=<ids>
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/pull-selected", [this, cilogic, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/pull-selected", [this, lubuntuci, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
@ -654,7 +667,7 @@ bool WebServer::start_server(quint16 port) {
}) })
); );
std::string msg = cilogic->queue_pull_tarball(cilogic->get_packageconfs_by_ids(repos), task_queue, job_statuses); std::string msg = lubuntuci->cilogic.queue_pull_tarball(lubuntuci->cilogic.get_packageconfs_by_ids(repos), task_queue, job_statuses);
return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size())); return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size()));
}); });
}); });
@ -662,7 +675,7 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /build-selected?repos=foo,bar,baz // /build-selected?repos=foo,bar,baz
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/build-selected", [this, cilogic, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/build-selected", [this, lubuntuci, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
@ -686,7 +699,7 @@ bool WebServer::start_server(quint16 port) {
}) })
); );
std::string msg = cilogic->queue_build_upload(cilogic->get_packageconfs_by_ids(repos), task_queue, job_statuses); std::string msg = lubuntuci->cilogic.queue_build_upload(lubuntuci->cilogic.get_packageconfs_by_ids(repos), task_queue, job_statuses);
return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size())); return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size()));
}); });
}); });
@ -694,7 +707,7 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /pull-and-build-selected?repos=foo,bar,baz // /pull-and-build-selected?repos=foo,bar,baz
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/pull-and-build-selected", [this, cilogic, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/pull-and-build-selected", [this, lubuntuci, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
@ -717,11 +730,11 @@ bool WebServer::start_server(quint16 port) {
return std::stoi(s); return std::stoi(s);
}) })
); );
auto pkgconfs = cilogic->get_packageconfs_by_ids(repos); auto pkgconfs = lubuntuci->cilogic.get_packageconfs_by_ids(repos);
for (auto pkgconf : pkgconfs) pkgconf->clear_tasks(); for (auto pkgconf : pkgconfs) pkgconf->clear_tasks();
std::string msg = cilogic->queue_pull_tarball(pkgconfs, task_queue, job_statuses); std::string msg = lubuntuci->cilogic.queue_pull_tarball(pkgconfs, task_queue, job_statuses);
msg += cilogic->queue_build_upload(pkgconfs, task_queue, job_statuses); msg += lubuntuci->cilogic.queue_build_upload(pkgconfs, task_queue, job_statuses);
return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size())); return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size()));
}); });
}); });
@ -729,13 +742,13 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /pull-all // /pull-all
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/pull-all", [this, cilogic, all_repos, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/pull-all", [this, lubuntuci, all_repos, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
} }
return QtConcurrent::run([=, this]() { return QtConcurrent::run([=, this]() {
std::string msg = cilogic->queue_pull_tarball(all_repos, task_queue, job_statuses); std::string msg = lubuntuci->cilogic.queue_pull_tarball(all_repos, task_queue, job_statuses);
return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size())); return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size()));
}); });
@ -744,13 +757,13 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /build-all // /build-all
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/build-all", [this, cilogic, all_repos, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/build-all", [this, lubuntuci, all_repos, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
} }
return QtConcurrent::run([=, this]() { return QtConcurrent::run([=, this]() {
std::string msg = cilogic->queue_build_upload(all_repos, task_queue, job_statuses); std::string msg = lubuntuci->cilogic.queue_build_upload(all_repos, task_queue, job_statuses);
return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size())); return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size()));
}); });
@ -759,15 +772,15 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /pull-and-build-all // /pull-and-build-all
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/pull-and-build-all", [this, cilogic, all_repos, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/pull-and-build-all", [this, lubuntuci, all_repos, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
} }
return QtConcurrent::run([=, this]() { return QtConcurrent::run([=, this]() {
for (auto pkgconf : all_repos) pkgconf->clear_tasks(); for (auto pkgconf : all_repos) pkgconf->clear_tasks();
std::string msg = cilogic->queue_pull_tarball(all_repos, task_queue, job_statuses); std::string msg = lubuntuci->cilogic.queue_pull_tarball(all_repos, task_queue, job_statuses);
msg += cilogic->queue_build_upload(all_repos, task_queue, job_statuses); msg += lubuntuci->cilogic.queue_build_upload(all_repos, task_queue, job_statuses);
return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size())); return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size()));
}); });
@ -776,7 +789,7 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// Serve static files from /static/<arg> // Serve static files from /static/<arg>
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/static/<arg>", [this, cilogic, job_statuses](const QString filename) -> QHttpServerResponse { http_server_.route("/static/<arg>", [this, lubuntuci, job_statuses](const QString filename) -> QHttpServerResponse {
QString sanitized_filename = filename; QString sanitized_filename = filename;
if (filename.contains("..") || filename.contains("../")) { if (filename.contains("..") || filename.contains("../")) {
return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest); return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);
@ -814,7 +827,7 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /graph // /graph
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/graph", [this, cilogic, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/graph", [this, lubuntuci, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
@ -862,7 +875,7 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /tasks // /tasks
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/tasks", [this, cilogic, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/tasks", [this, lubuntuci, job_statuses](const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
@ -898,7 +911,7 @@ bool WebServer::start_server(quint16 port) {
title_prefix = "Completed"; title_prefix = "Completed";
// gather tasks that have start_time > 0 and finish_time > 0 // gather tasks that have start_time > 0 and finish_time > 0
std::vector<std::shared_ptr<Task>> tasks_vector; std::vector<std::shared_ptr<Task>> tasks_vector;
auto pkgconfs = cilogic->get_packageconfs(); auto pkgconfs = lubuntuci->cilogic.get_packageconfs();
for (auto &pkgconf : pkgconfs) { for (auto &pkgconf : pkgconfs) {
for (auto &j : *job_statuses) { for (auto &j : *job_statuses) {
if (!j.second) { if (!j.second) {
@ -954,7 +967,7 @@ bool WebServer::start_server(quint16 port) {
////////////////////////////////////////// //////////////////////////////////////////
// /log/<TASK_ID> // /log/<TASK_ID>
////////////////////////////////////////// //////////////////////////////////////////
http_server_.route("/log/<arg>", [this, cilogic, job_statuses](const QString _task_id, const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> { http_server_.route("/log/<arg>", [this, lubuntuci, job_statuses](const QString _task_id, const QHttpServerRequest &req) -> QFuture<QHttpServerResponse> {
{ {
QHttpServerResponse session_response = verify_session_token(req, req.headers()); QHttpServerResponse session_response = verify_session_token(req, req.headers());
if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); }); if (session_response.statusCode() == StatusCodeFound) return QtConcurrent::run([response = std::move(session_response)]() mutable { return std::move(response); });
@ -971,12 +984,19 @@ bool WebServer::start_server(quint16 port) {
std::string msg = "<html><body><h1>Invalid task ID specified.</h1></body></html>"; std::string msg = "<html><body><h1>Invalid task ID specified.</h1></body></html>";
return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size())); return QHttpServerResponse("text/html", QByteArray(msg.c_str(), (int)msg.size()));
} }
std::string log_content = cilogic->get_task_log(task_id);
std::map<std::string, std::string> context; std::string log_content = lubuntuci->cilogic.get_task_log(task_id);
std::map<std::string, std::vector<std::map<std::string, std::string>>> list_context; std::map<std::string, std::vector<std::map<std::string, std::string>>> list_context;
std::map<std::string, std::string> context;
context["title"] = "Task Logs"; context["title"] = "Task Logs";
context["log"] = log_content; context["log"] = log_content;
std::string final_html = TemplateRenderer::render_with_inheritance("log.html", context, list_context);
std::string final_html = TemplateRenderer::render_with_inheritance(
"log.html",
context,
list_context
);
return QHttpServerResponse("text/html", QByteArray(final_html.c_str(), (int)final_html.size())); return QHttpServerResponse("text/html", QByteArray(final_html.c_str(), (int)final_html.size()));
}); });
}); });
@ -991,6 +1011,7 @@ bool WebServer::start_server(quint16 port) {
key_file.open(QIODevice::ReadOnly); key_file.open(QIODevice::ReadOnly);
ssl_config.setPrivateKey(QSslKey(&key_file, QSsl::Rsa, QSsl::Pem)); ssl_config.setPrivateKey(QSslKey(&key_file, QSsl::Rsa, QSsl::Pem));
key_file.close(); key_file.close();
ssl_config.setPeerVerifyMode(QSslSocket::VerifyNone); ssl_config.setPeerVerifyMode(QSslSocket::VerifyNone);
ssl_config.setProtocol(QSsl::TlsV1_3); ssl_config.setProtocol(QSsl::TlsV1_3);
ssl_server_.setSslConfiguration(ssl_config); ssl_server_.setSslConfiguration(ssl_config);