From 6f64fe2794ebc05586c87003924ed2d8c0b6aaad Mon Sep 17 00:00:00 2001 From: Joakim Roubert Date: Thu, 20 Feb 2025 16:49:29 +0100 Subject: [PATCH] Move up from legacy ACAP SDK to ACAP 4 Native SDK - Use FastCGI instead of legacy axhttp - Move to C++20, enables making code simpler Change-Id: I5d5c14c314b8296786f3a929f4f47952adfc819b Signed-off-by: Joakim Roubert --- Dockerfile | 6 +- Makefile | 22 ++--- include/CgiHandler.hpp | 27 +++--- manifest.json | 12 +-- src/CgiHandler.cpp | 170 ++++++++++++++++++++++---------------- src/opcuacolorchecker.cpp | 2 +- 6 files changed, 131 insertions(+), 108 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3337b47..e52096e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ ARG ARCH=aarch64 -ARG SDK_VERSION=3.5 -ARG SDK_IMAGE=axisecp/acap-sdk +ARG SDK_VERSION=12.2.0 +ARG SDK_IMAGE=axisecp/acap-native-sdk ARG DEBUG_WRITE ARG BUILD_DIR=/opt/build ARG ACAP_BUILD_DIR="$BUILD_DIR"/app ARG OPEN62541_VERSION=1.4.4 -ARG OPENCV_VERSION=4.5.5 +ARG OPENCV_VERSION=4.11.0 FROM $SDK_IMAGE:$SDK_VERSION-$ARCH AS builder diff --git a/Makefile b/Makefile index 3e8860a..93a6003 100644 --- a/Makefile +++ b/Makefile @@ -2,20 +2,22 @@ TARGET = opcuacolorchecker OBJECTS = $(wildcard $(CURDIR)/src/*.cpp) RM ?= rm -f -PKGS = gio-2.0 gio-unix-2.0 vdostream open62541 axevent axhttp axparameter +SDK_PKGS = axevent axparameter fcgi gio-2.0 gio-unix-2.0 open62541 vdostream +OWN_PKGS = opencv4 open62541 -CXXFLAGS += -Os -pipe -std=c++11 -Wall -Werror -Wextra -CXXFLAGS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --cflags-only-I $(PKGS)) -LDLIBS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --libs $(PKGS)) +CXXFLAGS += -Os -pipe -std=c++20 -Wall -Werror -Wextra +CXXFLAGS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --cflags-only-I $(SDK_PKGS)) +CXXFLAGS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) PKG_CONFIG_SYSROOT_DIR= pkg-config --cflags-only-I $(OWN_PKGS)) +LDLIBS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --libs $(SDK_PKGS)) +LDLIBS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) PKG_CONFIG_SYSROOT_DIR= pkg-config --libs $(SDK_PKGS) $(OWN_PKGS)) -CXXFLAGS += -I$(SDKTARGETSYSROOT)/usr/include/opencv4 -I$(CURDIR)/include +CXXFLAGS += -I$(CURDIR)/include LDFLAGS = -L./lib -Wl,--no-as-needed,-rpath,'$$ORIGIN/lib' -flto=auto -LDLIBS += -lm -lopencv_core -lopencv_imgproc -lopencv_video -lpthread +#LDLIBS += -lm -lpthread # Set DEBUG_WRITE to write debug images to storage ifneq ($(DEBUG_WRITE),) CXXFLAGS += -DDEBUG_WRITE -LDLIBS += -lopencv_imgcodecs DOCKER_ARGS += --build-arg DEBUG_WRITE=$(DEBUG_WRITE) endif @@ -24,14 +26,14 @@ endif all: $(TARGET) $(TARGET): $(OBJECTS) - $(CXX) $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) $^ -o $@ && \ + $(CXX) $(CXXFLAGS) $^ -o $@ $(LDFLAGS) $(LDLIBS) && \ $(STRIP) --strip-unneeded $@ -# docker build container targets +# Docker build container targets %.eap: DOCKER_BUILDKIT=1 docker build $(DOCKER_ARGS) --build-arg ARCH=$(*F) -o type=local,dest=. "$(CURDIR)" -dockerbuild: armv7hf.eap aarch64.eap +dockerbuild: aarch64.eap armv7hf.eap clean: $(RM) $(TARGET) *.eap* *_LICENSE.txt pa*.conf diff --git a/include/CgiHandler.hpp b/include/CgiHandler.hpp index 998b554..3f421e2 100644 --- a/include/CgiHandler.hpp +++ b/include/CgiHandler.hpp @@ -16,31 +16,32 @@ #pragma once -#include +#include +#include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat" #include +#pragma GCC diagnostic pop +#include class CgiHandler { public: CgiHandler(cv::Scalar (*GetColor)(), gboolean (*GetColorAreaValue)(), gboolean (*PickCurrentCallback)()); ~CgiHandler(); - static void RequestHandler( - const gchar *path, - const gchar *method, - const gchar *query, - GHashTable *params, - GOutputStream *output_stream, - gpointer user_data); private: - static void - WriteErrorResponse(GDataOutputStream &dos, const guint32 statuscode, const gchar *statusname, const gchar *msg); - static void WriteBadRequest(GDataOutputStream &dos, const gchar *msg); - static void WriteInternalError(GDataOutputStream &dos, const gchar *msg); + void Run(); + gboolean HandleFcgiRequest(); + + static void WriteResponse(FCGX_Stream &stream, const guint32 status_code, const gchar *mimetype, const gchar *msg); cv::Scalar (*GetColor_)(); gboolean (*GetColorAreaValue_)(); gboolean (*PickCurrentCallback_)(); - AXHttpHandler *http_handler_; + FCGX_Request request_; + bool running_; + int sock_; + std::jthread worker_; }; diff --git a/manifest.json b/manifest.json index 5221dc5..0da22f0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,24 +1,20 @@ { - "schemaVersion": "1.3", + "schemaVersion": "1.7.3", "acapPackageConf": { "setup": { "friendlyName": "OPC UA Color Checker", "appName": "opcuacolorchecker", "vendor": "Axis Communications AB", "embeddedSdkVersion": "3.0", - "user": { - "username": "sdk", - "group": "sdk" - }, "vendorUrl": "https://www.axis.com/", "runMode": "respawn", - "version": "1.1.3" + "version": "2.0.0" }, "configuration": { "settingPage": "settings.html", "httpConfig": [ - {"type": "transferCgi", "name": "getstatus.cgi", "access": "viewer"}, - {"type": "transferCgi", "name": "pickcurrent.cgi", "access": "admin"} + {"type": "fastCgi", "name": "getstatus.cgi", "access": "viewer"}, + {"type": "fastCgi", "name": "pickcurrent.cgi", "access": "admin"} ], "paramConfig": [ {"name": "CenterX", "type": "int:min=0,max=959", "default": "100"}, diff --git a/src/CgiHandler.cpp b/src/CgiHandler.cpp index 9fadf45..7d8475f 100644 --- a/src/CgiHandler.cpp +++ b/src/CgiHandler.cpp @@ -15,109 +15,133 @@ */ #include -#include -#include +#include +#include #include +#include #include "CgiHandler.hpp" #include "ColorArea.hpp" #include "common.hpp" -using namespace std; +#define FCGI_SOCKET_NAME "FCGI_SOCKET_NAME" + +#define MIME_JSON "application/json" +#define MIME_TEXT "text/plain" CgiHandler::CgiHandler(cv::Scalar (*GetColor)(), gboolean (*GetColorAreaValue)(), gboolean (*PickCurrentCallback)()) : GetColor_(GetColor), GetColorAreaValue_(GetColorAreaValue), PickCurrentCallback_(PickCurrentCallback), - http_handler_(ax_http_handler_new(this->RequestHandler, this)) + running_(false) { assert(nullptr != GetColor_); assert(nullptr != GetColorAreaValue_); assert(nullptr != PickCurrentCallback_); - assert(nullptr != http_handler_); -} -CgiHandler::~CgiHandler() -{ - LOG_I("%s/%s: Free http handler ...", __FILE__, __FUNCTION__); - assert(nullptr != http_handler_); - ax_http_handler_free(http_handler_); -} + LOG_I("Setting up FastCGI ..."); + auto socket_path = getenv(FCGI_SOCKET_NAME); + if (nullptr == socket_path) + { + LOG_E("Failed to get environment variable FCGI_SOCKET_NAME"); + assert(false); + } -void CgiHandler::WriteErrorResponse( - GDataOutputStream &dos, - const guint32 statuscode, - const gchar *statusname, - const gchar *msg) -{ - ostringstream ss; - ss << "Status: " << statuscode << " " << statusname << "\r\n" - << "Content-Type: text/html\r\n" - << "\r\n" - << "" << statuscode << " " << statusname << "\n" - << "

" << statuscode << " " << statusname << "

\n" - << msg << "\n" - << "\n"; - - g_data_output_stream_put_string(&dos, ss.str().c_str(), nullptr, nullptr); + LOG_I("Using socket path %s", socket_path); + if (0 != FCGX_Init()) + { + LOG_E("FCGX_Init failed"); + assert(false); + } + + sock_ = FCGX_OpenSocket(socket_path, 5); + chmod(socket_path, S_IRWXU | S_IRWXG | S_IRWXO); + if (0 != FCGX_InitRequest(&request_, sock_, 0)) + { + LOG_E("FCGX_InitRequest failed"); + assert(false); + } + + LOG_I("Set up FastCGI for %s, start handling incoming CGI requests ...", socket_path); + worker_ = std::jthread(&CgiHandler::Run, this); } -void CgiHandler::WriteBadRequest(GDataOutputStream &dos, const gchar *msg) +CgiHandler::~CgiHandler() { - WriteErrorResponse(dos, 400, "Bad Request", msg); + // std::jthread automatically joins, so we only need to set running_ to false + LOG_I("Stop CGI handling ..."); + running_ = false; + LOG_I("Shutting down FastCGI ..."); + close(sock_); } -void CgiHandler::WriteInternalError(GDataOutputStream &dos, const gchar *msg) +void CgiHandler::Run() { - WriteErrorResponse(dos, 500, "Internal Server Error", msg); + running_ = true; + while (running_) + { + HandleFcgiRequest(); + } } -void CgiHandler::RequestHandler( - const gchar *path, - const gchar *method, - const gchar *query, - GHashTable *params, - GOutputStream *output_stream, - gpointer user_data) +gboolean CgiHandler::HandleFcgiRequest() { - (void)method; - (void)query; - (void)params; + if (0 == FCGX_Accept_r(&request_)) + { + LOG_I("FCGX_Accept_r OK"); - auto dos = g_data_output_stream_new(output_stream); - assert(nullptr != dos); - assert(nullptr != user_data); - auto cgi_handler = static_cast(user_data); + // Extract the CGI call only from the request + const auto command = std::filesystem::path(FCGX_GetParam("SCRIPT_NAME", request_.envp)).filename().string(); - const auto func = basename(const_cast(path)); - if (0 == strcmp("getstatus.cgi", func)) - { - assert(nullptr != cgi_handler->GetColorAreaValue_); - const auto status = cgi_handler->GetColorAreaValue_(); - g_data_output_stream_put_string(dos, "Status: 200 OK\r\n", nullptr, nullptr); - g_data_output_stream_put_string(dos, "Content-Type: application/json\r\n\r\n", nullptr, nullptr); - ostringstream ss; - ss << "{\"status\": " << (status ? "true" : "false") << "}" << endl; - g_data_output_stream_put_string(dos, ss.str().c_str(), nullptr, nullptr); - } - else if (0 == strcmp("pickcurrent.cgi", func)) - { - assert(nullptr != cgi_handler->PickCurrentCallback_); - if (!cgi_handler->PickCurrentCallback_()) + if ("getstatus.cgi" == command) { - WriteInternalError(*dos, "Failed to pick current color"); - goto http_exit; + assert(nullptr != GetColorAreaValue_); + const auto status_json = std::format("{{\"status\": {:b}}}", GetColorAreaValue_()); + WriteResponse(*request_.out, 200, MIME_JSON, status_json.c_str()); + } + else if ("pickcurrent.cgi" == command) + { + assert(nullptr != PickCurrentCallback_); + if (!PickCurrentCallback_()) + { + WriteResponse(*request_.out, 500, MIME_TEXT, "Failed to pick current color"); + } + else + { + const auto color = GetColor_(); + const auto color_json = std::format(R"({{"R": {}, "G": {}, "B": {}}})", color[R], color[G], color[B]); + WriteResponse(*request_.out, 200, MIME_JSON, color_json.c_str()); + } + } + else + { + assert(nullptr != request_.out); + WriteResponse(*request_.out, 400, MIME_TEXT, std::format("Unknown command '{}'", command).c_str()); } - const auto color = cgi_handler->GetColor_(); - g_data_output_stream_put_string(dos, "Status: 200 OK\r\n", nullptr, nullptr); - g_data_output_stream_put_string(dos, "Content-Type: application/json\r\n\r\n", nullptr, nullptr); - ostringstream ss; - ss << "{\"R\":" << color[R] << ", \"G\":" << color[G] << ", \"B\":" << color[B] << "}" << endl; - g_data_output_stream_put_string(dos, ss.str().c_str(), nullptr, nullptr); + FCGX_Finish_r(&request_); } - else + return G_SOURCE_CONTINUE; +} + +void CgiHandler::WriteResponse(FCGX_Stream &stream, const guint32 status_code, const gchar *mimetype, const gchar *msg) +{ + std::string descr; + switch (status_code) { - WriteBadRequest(*dos, "Unknown action"); + case 200: + descr = "OK"; + break; + case 400: + descr = "Bad Request"; + break; + case 500: + descr = "Internal Server Error"; + break; + default: + LOG_E("%s/%s: Error code %u not yet implemented", __FILE__, __FUNCTION__, status_code); + assert(false); + break; } -http_exit: - g_object_unref(dos); + const std::string response_text = + std::format("Status: {} {}\r\nContent-Type: {}\r\n\r\n{}", status_code, descr.c_str(), mimetype, msg); + FCGX_FPrintF(&stream, response_text.c_str()); } diff --git a/src/opcuacolorchecker.cpp b/src/opcuacolorchecker.cpp index 41243ef..dd8e211 100644 --- a/src/opcuacolorchecker.cpp +++ b/src/opcuacolorchecker.cpp @@ -321,7 +321,7 @@ int main(int argc, char *argv[]) goto exit_param; } - LOG_I("Start main loop ..."); + LOG_I("Create and start main loop ..."); assert(nullptr == loop); loop = g_main_loop_new(nullptr, FALSE); g_main_loop_run(loop);