From a1566d1ca182c1714fb5cdb4540adc9afacef5bf Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Fri, 17 Jul 2020 15:34:59 +0200 Subject: Add GOMA support --- meson.build | 10 +- src/goma.cc | 339 +++++++++++++++++++++++++++++++++++++++++++++ src/goma.hh | 12 ++ src/main.cc | 32 +++-- subprojects/.gitignore | 2 + subprojects/rapidjson.wrap | 10 ++ 6 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 src/goma.cc create mode 100644 src/goma.hh create mode 100644 subprojects/.gitignore create mode 100644 subprojects/rapidjson.wrap diff --git a/meson.build b/meson.build index 4affa35..76f8779 100644 --- a/meson.build +++ b/meson.build @@ -37,6 +37,10 @@ dep_cairo = dependency('cairo-xcb', version : '>= 1.12.0') dep_pango = dependency('pangocairo', version : '>= 1.36.8') dep_icecc = dependency('icecc', version : '>= 1.1') dep_thread = dependency('threads') +dep_curl = dependency('libcurl', version : '>= 7.0') + +dep_json = dependency('rapidjson', version : '>= 1.1', + fallback: ['rapidjson', 'rapidjson_dep']) configure_file(output: 'config.h', configuration: conf) @@ -46,7 +50,7 @@ executable('monmon', 'src/poll_looper.cc', 'src/looper.cc', 'src/clock.cc', 'src/io.cc', 'src/monitor.cc', 'src/fake_monitor.cc', 'src/animator.cc', 'src/blissful_monitor.cc', - 'src/monmon.cc', 'src/icecc.cc'], - dependencies: [dep_thread, dep_pango, dep_cairo, dep_icecc, - xcb_deps], + 'src/monmon.cc', 'src/icecc.cc', 'src/goma.cc'], + dependencies: [dep_thread, dep_pango, dep_cairo, dep_icecc, dep_curl, + dep_json, xcb_deps], install: true) diff --git a/src/goma.cc b/src/goma.cc new file mode 100644 index 0000000..717d7b1 --- /dev/null +++ b/src/goma.cc @@ -0,0 +1,339 @@ +#include "common.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "args.hh" +#include "cairo.hh" +#include "goma.hh" +#include "monmon.hh" +#include "pango.hh" + +namespace { + +struct CurlDelete { + void operator()(CURL* curl) const { + curl_easy_cleanup(curl); + } +}; + +struct State { + bool error_{true}; + std::string msg_; + uint64_t running_{0}; + uint64_t failed_{0}; +}; + +class GomaPoll { +public: + GomaPoll(std::shared_ptr const& looper, std::string addr, + std::function update) + : looper_(looper), addr_(std::move(addr)), update_(update) { + pipe_.open(); + looper_->add(pipe_.read(), Looper::EV_READ, + std::bind(&GomaPoll::pipe, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3)); + thread_ = std::thread(&GomaPoll::loop, this); + } + + ~GomaPoll() { + looper_->remove(pipe_.read()); + { + std::lock_guard lock(mutex_); + quit_ = true; + } + cond_.notify_all(); + thread_.join(); + } + + State const& state() const { + return state_; + } + +private: + void loop() { + std::unique_ptr curl(curl_easy_init()); + + uint64_t after = 0; + std::unordered_set active; + std::unordered_set failed; + + curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, + &GomaPoll::write_callback); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, this); + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, ""); + curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 10L); + + while (true) { + State state; + content_.clear(); + std::string url = addr_; + if (after > 0) + url += "?after=" + std::to_string(after); + curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); + auto ret = curl_easy_perform(curl.get()); + if (ret == CURLE_OK) { + parse(content_, &state, &active, &failed, &after); + } else { + state.error_ = true; + state.msg_ = curl_easy_strerror(ret); + } + + { + std::unique_lock lock(mutex_); + state_ = state; + io::write(pipe_, "s", 1); + cond_.wait_for(lock, std::chrono::seconds(1)); + if (quit_) + break; + } + } + } + + static void parse(std::string const& content, State* state, + std::unordered_set* active, + std::unordered_set* failed, + uint64_t* after) { + *after = 0; + state->error_ = false; + + rapidjson::Document doc; + doc.Parse(content.data(), content.size()); + if (doc.HasParseError() || !doc.IsObject()) { + state->error_ = true; + state->msg_ = rapidjson::GetParseError_En(doc.GetParseError()); + return; + } + if (doc.HasMember("last_update_ms")) { + auto& member = doc["last_update_ms"]; + if (member.IsUint64()) { + *after = member.GetUint64(); + } + } + if (doc.HasMember("active")) { + auto& member = doc["active"]; + if (member.IsArray()) { + for (auto& task : member.GetArray()) { + if (task.HasMember("id")) { + auto& id = task["id"]; + if (id.IsUint64()) { + active->insert(id.GetUint64()); + } + } + } + } + } + if (doc.HasMember("failed")) { + auto& member = doc["failed"]; + if (member.IsArray()) { + for (auto& task : member.GetArray()) { + if (task.HasMember("id")) { + auto& id = task["id"]; + if (id.IsUint64()) { + failed->insert(id.GetUint64()); + } + } + } + } + } + if (doc.HasMember("finished")) { + auto& member = doc["finished"]; + if (member.IsArray()) { + for (auto& task : member.GetArray()) { + if (task.HasMember("id")) { + auto& id = task["id"]; + if (id.IsUint64()) { + active->erase(id.GetUint64()); + } + } + } + } + } + + state->running_ = active->size(); + state->failed_ = failed->size(); + } + + static size_t write_callback(char* ptr, size_t size, size_t nmemb, + void* userdata) { + return reinterpret_cast(userdata)->write(ptr, size * nmemb); + } + + size_t write(char* ptr, size_t size) { + content_.append(ptr, size); + return size; + } + + void pipe(Looper*, int, uint8_t event) { + if (event & Looper::EV_READ) { + char tmp; + if (io::read(pipe_, &tmp, 1)) { + assert(tmp == 's'); + update_(); + } + } + } + + std::shared_ptr looper_; + io::pipe pipe_; + std::string const addr_; + std::thread thread_; + std::function const update_; + + std::mutex mutex_; + std::condition_variable cond_; + bool quit_{false}; + State state_; + + std::string content_; +}; + +class GomaMonMon : public MonMon { +public: + GomaMonMon(std::shared_ptr const& looper) + : MonMon(looper) { + curl_global_init(CURL_GLOBAL_ALL); + } + + ~GomaMonMon() { + curl_global_cleanup(); + } + + bool connect(Args const* args) override { + std::string addr(args->arg("goma_addr", "http://127.0.0.1:8088/")); + if (addr.empty() || addr.back() != '/') + addr += '/'; + addr += "api/taskz"; + + poll_ = std::make_unique( + looper_, std::move(addr), + std::bind(&GomaMonMon::draw, this)); + + return true; + } + +private: + std::string title() const override { + return "MonMon (goma)"; + } + + void width_changed() override { + progress_pattern_.reset(); + } + + void internal_quit() override { + poll_.reset(); + } + + void draw_content(cairo_t* cairo, PangoLayout* layout, + uint16_t w, uint16_t h) override { + if (!poll_) return; + auto state = poll_->state(); + + auto const pad_x = std::min(9.0, w / 20.0), pad_y = pad_x; + auto const margin_x = std::min(7.5, w / 20.0), margin_y = margin_x; + auto const box_w = w - pad_x * 2; + if (!progress_pattern_) { + progress_pattern_.reset( + cairo_pattern_create_linear(0.0, 0.0, box_w, 0.0)); + cairo_pattern_add_color_stop_rgb(progress_pattern_.get(), 0.000000, + 0.000000, 0.000000, 0.000000); + cairo_pattern_add_color_stop_rgb(progress_pattern_.get(), 0.594324, + 0.729412, 0.000000, 0.000000); + cairo_pattern_add_color_stop_rgb(progress_pattern_.get(), 0.809683, + 1.000000, 0.545098, 0.196078); + cairo_pattern_add_color_stop_rgb(progress_pattern_.get(), 0.899833, + 0.972549, 0.937255, 0.074510); + cairo_pattern_add_color_stop_rgb(progress_pattern_.get(), 1.000000, + 0.976471, 0.968627, 0.831373); + } + pango_layout_set_width(layout, (box_w - margin_x * 2) * PANGO_SCALE); + + auto x = pad_x; + auto y = pad_y; + + if (state.error_) { + draw_box(cairo, layout, x, y, box_w, margin_y, 1.0, 0.0, 0.0, state.msg_, + 0.0); + } else { + float progress; + if (state.running_ > max_running_) { + max_running_ = state.running_; + progress = 1.0f; + } else { + progress = static_cast(state.running_) / max_running_; + } + std::string msg = "Active: " + std::to_string(state.running_); + y = draw_box(cairo, layout, x, y, box_w, margin_y, 1.0, 1.0, 1.0, msg, + progress) + + pad_y; + if (state.failed_ > 0 && y < h) { + msg = "Failed: " + std::to_string(state.failed_); + draw_box(cairo, layout, x, y, box_w, margin_y, 1.0, 0, 0, msg, 0); + } + } + } + + uint16_t draw_box(cairo_t* cairo, PangoLayout* layout, + uint16_t x, uint16_t y, + uint16_t w, uint16_t margin_y, + float r, float g, float b, + std::string const& text, + float progress) { + pango_layout_set_text(layout, text.data(), text.size()); + int layout_height; + pango_layout_get_size(layout, nullptr, &layout_height); + uint16_t height = (layout_height + PANGO_SCALE - 1) / PANGO_SCALE; + rounded_path(cairo, x, y, w, height + margin_y * 2); + cairo_set_source_rgba(cairo, 0.1, 0.1, 0.1, 0.7); + + if (progress > 0.0) { + cairo_fill_preserve(cairo); + auto old = cairo::unique_path(cairo_copy_path(cairo)); + cairo_new_path(cairo); + cairo_rectangle(cairo, x, y, progress * w, height + margin_y * 2); + cairo_clip(cairo); + cairo_append_path(cairo, old.get()); + cairo_matrix_t matrix; + cairo_matrix_init_translate(&matrix, -x, 0); + cairo_pattern_set_matrix(progress_pattern_.get(), &matrix); + cairo_set_source(cairo, progress_pattern_.get()); + cairo_fill(cairo); + cairo_reset_clip(cairo); + } else { + cairo_fill(cairo); + } + + cairo_move_to(cairo, x, y + margin_y); + pango_cairo_layout_path(cairo, layout); + cairo_set_source_rgb(cairo, 0.0, 0.0, 0.0); + cairo_set_line_cap(cairo, CAIRO_LINE_CAP_ROUND); + cairo_set_line_width(cairo, 2.0); + cairo_stroke(cairo); + cairo_set_source_rgb(cairo, r, g, b); + cairo_move_to(cairo, x, y + margin_y); + pango_cairo_show_layout(cairo, layout); + + return y + margin_y * 2 + height; + } + + uint64_t max_running_{10}; + cairo::unique_pattern progress_pattern_; + std::unique_ptr poll_; +}; + +} // namespace + +std::unique_ptr create_goma_monmon( + std::shared_ptr const& looper) { + return std::make_unique(looper); +} + diff --git a/src/goma.hh b/src/goma.hh new file mode 100644 index 0000000..9c4fe3e --- /dev/null +++ b/src/goma.hh @@ -0,0 +1,12 @@ +#ifndef GOMA_HH +#define GOMA_HH + +#include + +class MonMon; +class PollLooper; + +std::unique_ptr create_goma_monmon( + std::shared_ptr const& looper); + +#endif // GOMA_HH diff --git a/src/main.cc b/src/main.cc index 4bcbdd3..096b0f3 100644 --- a/src/main.cc +++ b/src/main.cc @@ -8,8 +8,9 @@ #include "animator.hh" #include "args.hh" -#include "io.hh" +#include "goma.hh" #include "icecc.hh" +#include "io.hh" #include "monmon.hh" #include "poll_looper.hh" #include "x.hh" @@ -142,31 +143,37 @@ void xcb_event_loop(x::shared_connection const& conn, int main(int argc, char** argv) { std::unique_ptr args(Args::create()); - args->add("network", "NETNAME", - "monitor NETNAME instead of default ICECREAM"); - args->add("scheduler", "HOST", - "connect to scheduler at HOST instead of broadcasting"); args->add("display", "HOST:VS", "connect to X server at [HOST]:[VS]"); args->add("no-render", "do not use render extension even if available"); args->add("titlebar", "create a normal window instead of the default without" " titlebar (or other WM addons)"); args->add("black", "use black background instead of background pixmap"); - args->add("columns", "COLUMNS", "force columns to be COLUMNS instead of" - " calculated from window size"); args->add("help", "display this text and exit."); + args->add("network", "NETNAME", + "monitor NETNAME instead of default ICECREAM (icecc only)"); + args->add("scheduler", "HOST", + "connect to scheduler at HOST instead of broadcasting" + " (icecc only)"); + args->add("columns", "COLUMNS", "force columns to be COLUMNS instead of" + " calculated from window size (icecc only)"); + args->add("goma", "start GOMA monitor instead of icecc"); + args->add("goma_addr", "ADDR", + "address to connect to instead of default" + " http://localhost:8088 (goma only)"); if (!args->run(argc, argv)) { std::cout << "Try `monmon --help` for usage." << std::endl; return EXIT_FAILURE; } if (args->is_set("help")) { std::cout << "Usage: `monmon [OPTIONS...]`\n" - << "icecream (icecc) monitor\n" + << "icecream (icecc) or goma monitor\n" << "\n"; args->print_help(std::cout); return EXIT_FAILURE; } + bool goma = args->is_set("goma"); unsigned columns = 0; - { + if (!goma) { auto tmp = args->arg("columns", nullptr); if (tmp) { char* end = nullptr; @@ -200,7 +207,12 @@ int main(int argc, char** argv) { atoms->preload("__MONMON_QUIT"); MonMon::preload(atoms.get()); std::shared_ptr ewmh(x::Ewmh::create(conn, screen_index)); - std::shared_ptr monmon(create_icecc_monmon(looper, columns)); + std::shared_ptr monmon; + if (goma) { + monmon = create_goma_monmon(looper); + } else { + monmon = create_icecc_monmon(looper, columns); + } monmon->init(conn, screen, format, atoms, ewmh, 400, 400, args->is_set("titlebar"), args->is_set("black")); if (!monmon->connect(args.get())) diff --git a/subprojects/.gitignore b/subprojects/.gitignore new file mode 100644 index 0000000..d8fa25a --- /dev/null +++ b/subprojects/.gitignore @@ -0,0 +1,2 @@ +packagecache/ +rapidjson-1.1.0/ diff --git a/subprojects/rapidjson.wrap b/subprojects/rapidjson.wrap new file mode 100644 index 0000000..6273322 --- /dev/null +++ b/subprojects/rapidjson.wrap @@ -0,0 +1,10 @@ +[wrap-file] +directory = rapidjson-1.1.0 + +source_url = https://github.com/Tencent/rapidjson/archive/v1.1.0.zip +source_filename = rapidjson-1.1.0.zip +source_hash = 8e00c38829d6785a2dfb951bb87c6974fa07dfe488aa5b25deec4b8bc0f6a3ab + +patch_url = https://wrapdb.mesonbuild.com/v1/projects/rapidjson/1.1.0/1/get_zip +patch_filename = rapidjson-1.1.0-1-wrap.zip +patch_hash = 7e651680dc74da51b4d0ca4bd3de5d1cf2143dd6d81421b97a43d67536a84486 -- cgit v1.3