This commit is contained in:
fwastring 2025-10-13 10:38:50 +02:00
commit ab9a0bd4e2
183 changed files with 20701 additions and 0 deletions

Binary file not shown.

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
*.d
*.o
src/cli/cli
# VSCode
.vscode

8
LICENSE Normal file
View file

@ -0,0 +1,8 @@
Bredbandskollen CLI - A bandwidth measurement tool
Copright (C) 2018 The Swedish Internet Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

142
README.md Normal file
View file

@ -0,0 +1,142 @@
# Bredbandskollen CLI
The `src` directory contains the source code to the command line version of
Bredbandskollen CLI, a bandwidth measurement tool.
# How to build Bredbandskollen's CLI client
On Windows, open
src/wincli/wincli.sln
in Visual Studio 2015 or later, then choose "Build".
On all other platforms, change to the directory
src/cli
and run the command "make" (or "gmake"). GNU Make and a compiler with support
for C++11 is required, e.g. GCC version 4.7 or later or LLVM Clang version 3.9 or later.
To use a specific compiler, do e.g.
make CXX=clang++
To enable support for TLS/SSL, install GnuTLS version 3.5 or later, and do
make clean
make GNUTLS=1
To perform a bandwidth measurement using TLS, do
./cli --test --ssl
For more information, see "Platform Notes" below.
# How to run the CLI client
To perform a mesurement, simply run the executable program that was built using
the above steps. For more information, run it with the --help argument or read
https://frontend.bredbandskollen.se/download/README.txt
# About the source code
The directories framework and http contain a basic C++ network programming
framework with support for "tasks" and "timers". Some of the features are
explained by demo programs in the examples directory.
The [API documentation](https://www.dsso.se/bbkapi/annotated.html)
can be built from the source code using [Doxygen](https://www.doxygen.nl/).
The directory `json11` contains a JSON library for C++ provided by Dropbox, Inc.
The directory `measurement` contains the bandwidth measurement engine, built atop
the framework.
The directory `cli` contains a command line interface to the measurement engine.
The directory `qt5gui` contains the source code for a GUI to the measurement
engine. To build it, Qt5 and QWebEngine are required. You must run the Qt5
version of `qmake` to create a Makefile before running `make` to build the GUI.
# Platform Notes
* Windows
The code has not been thoroughly tested on Windows. Pull requests are welcome.
Visual Studio 2015 or later is required, as are the components
MSVC v140 (VS 2015 C++ build tools) and Windows 10 SDK. Visual Studio 2022 Community
can be downloaded from https://visualstudio.microsoft.com/
Open src/wincli/wincli.sln in Visual Studio, then select "Build".
* MacOS
Install Xcode from App Store, then go to src/cli and do make.
For SSL support, install Homebrew from https://brew.sh and then do
brew install gnutls
Once GnuTLS is installed, go to src/cli and do
make clean
make GNUTLS=1
* Linux
Make sure g++ version 4.7 or later is installed. Then go to src/cli and do
make
If GnuTLS version 3.5 or later is installed, including development files, do
make clean
make GNUTLS=1
* OpenWrt
OpenWrt is also Linux, but will generally include cross-compiling.
See [here](https://openwrt.org/docs/guide-developer/toolchain/crosscompile) for setting up the OpenWrt toolchain and getting ready to compile.
After setting the `STAGING_DIR` environment variable and adding the cross-compiler bin folder to `PATH`, note the name of the g++ executable there and run make with CXX set to it, e.g.:
make CXX=aarch64-openwrt-linux-musl-g++
* OpenBSD
Install gmake, llvm and gnutls using pkg_add, then go to src/cli and do
gmake
or
gmake GNUTLS=1
* FreeBSD
Install gmake and gnutls using pkg, then go to src/cli and do
gmake
or
gmake GNUTLS=1
* NetBSD
Install gmake, llvm, clang, and gnutls using pkgin, then go to src/cli and do
gmake
or
gmake GNUTLS=1
# License
Copright © 2018 The Swedish Internet Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

3
doc/Makefile Normal file
View file

@ -0,0 +1,3 @@
api:
cd .. && \
doxygen doc/doxygen.cfg

2660
doc/doxygen.cfg Normal file

File diff suppressed because it is too large Load diff

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1735563628,
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

65
flake.nix Normal file
View file

@ -0,0 +1,65 @@
{
description = "Bredbandskollen CLI";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
version = if self ? rev && self.rev != null then self.rev else "unstable";
in
{
packages = {
default = pkgs.stdenv.mkDerivation {
pname = "bbk";
inherit version;
src = self;
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.gnutls ];
makeFlags = [ "GNUTLS=1" ];
buildPhase = ''
runHook preBuild
make -C src/cli
runHook postBuild
'';
installPhase = ''
runHook preInstall
install -Dm755 src/cli/cli $out/bin/bbk
runHook postInstall
'';
meta = {
description = "Swedish Internet Foundation's Bredbandskollen CLI bandwidth measurement tool";
homepage = "https://www.bredbandskollen.se/";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ "Fredrik Wastring" ];
mainProgram = "bbk";
};
};
};
devShells.default = pkgs.mkShell {
buildInputs = [
pkgs.gnutls
pkgs.pkg-config
pkgs.gnumake
];
};
nixosModules.default =
{ pkgs, ... }:
{
environment.systemPackages = [ self.packages.${pkgs.system}.default ];
};
}
);
}

1
result Symbolic link
View file

@ -0,0 +1 @@
/nix/store/nwh5jhgbd3yq2bnryjf1kq6fhbsanm41-bredbandskollen-cli-unstable

2
src/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.o
*.d

1
src/cli/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
cli

31
src/cli/Makefile Normal file
View file

@ -0,0 +1,31 @@
TARGET = cli
DIRLEVEL = ..
# Possible LOGLEVEL values: dbg, info, warn, err, none
LOGLEVEL=info
# Uncomment if GnuTLS version 3.5 or later is available
# GNUTLS=1
# Uncomment this to avoid creating new processes
# NO_EXTERNAL_CMD=1
SOURCES=../http/cookiefile.cpp \
main.cpp \
../measurement/wsdownloadtask.cpp \
utils.cpp \
cliclient.cpp
ifeq ($(SERVER),1)
SOURCES += ../server/measurementserver.cpp \
../server/ticketclient.cpp
CXXFLAGS += -DRUN_SERVER
else
CLEAN += ../server/measurementserver.o ../server/ticketclient.o
endif
ifeq ($(NO_EXTERNAL_CMD),1)
CXXFLAGS += -DNO_EXTERNAL_CMD
endif
include $(DIRLEVEL)/measurement/mk.inc

455
src/cli/cliclient.cpp Normal file
View file

@ -0,0 +1,455 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#ifdef _WIN32
#define NOMINMAX
#include <winsock2.h>
#include <windows.h>
#include <iso646.h>
#include <stdio.h>
#include <io.h>
#else
#include <unistd.h>
#endif
#include <exception>
#include <iostream>
#include <iomanip>
#include <fstream>
#include "../json11/json11.hpp"
#include "cliclient.h"
CliClient::CliClient(const TaskConfig &config) :
Logger("CLI"),
out(&std::cout),
the_config(config) {
//setlocale(LC_ALL, "");
#ifdef _WIN32
out_is_tty = _isatty(_fileno(stdout));
#else
out_is_tty = isatty(fileno(stdout));
#endif
out_quiet = (the_config.value("quiet") == "1" ||
the_config.value("logfile") == "-");
if (!the_config.value("out").empty()) {
out = new std::ofstream(the_config.value("out"), std::ofstream::app);
out_is_tty = false;
}
report.measurement_server = the_config.value("server");
if (the_config.value("pingsweep") != "1" &&
!the_config.hasKey("list_measurements")) {
if (out_quiet) {
// Block all output:
out->clear(std::istream::eofbit);
} else {
*out << "Start: ";
sayTime(*out);
*out << ((the_config.value("mtype") == "ipv6") ? " [ipv6]\n" : "\n");
}
}
/*
try {
current_line.imbue(std::locale(""));
} catch (std::exception &e) {
log() << "cannot set locale: " << e.what();
}
*/
if (the_config.value("logfile") == "-")
out_is_tty = false; // Otherwise output might be garbled by the log.
}
void CliClient::initialMsgToAgent(std::deque<std::string> &return_msgs) {
return_msgs.push_back("{\"method\": \"clientReady\", \"args\": {}}");
}
void CliClient::newEventFromAgent(std::deque<std::string> &return_msgs,
const std::string &msg) {
std::string jsonerr;
auto obj = json11::Json::parse(msg, jsonerr);
if (!jsonerr.empty()) {
if (BridgeTask::isAgentTerminatedMessage(msg))
std::cerr << msg << std::endl;
else
err_log() << "JSON error: got " << msg;
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
return;
}
std::string event = obj["event"].string_value();
auto arg_obj = obj["args"];
if (event == "setInfo" && !arg_obj["logText"].string_value().empty()) {
log() << "EVENT: setInfo logText";
} else
log() << "EVENT: " << event << ' ' << msg;
if (event == "configuration") {
if (!arg_obj["require_consent"].string_value().empty()) {
// We need user consent to process personal data.
// First fetch the legalese:
json11::Json args = json11::Json::object {
{ "lang", "en" },
{ "format", "text" },
{ "consent", arg_obj["require_consent"] },
};
return_msgs.push_back(BridgeTask::msgToAgent("getContent", args.dump()));
// When the content arrives, we'll show it to the user and
// ask for consent.
return;
}
report.isp = arg_obj["ispname"].string_value();
if (!report.isp.empty())
*out << "Network operator: " << report.isp << std::endl;
if (the_config.value("ssl") == "1")
report.tls = 1;
int server_port = std::stoi(the_config.value("port"));
if (report.measurement_server.empty()) {
std::vector<json11::Json> vec = arg_obj["servers"].array_items();
for (auto srv : vec) {
if (srv["type"].string_value() == the_config.value("mtype")) {
std::string hostname = srv["url"].string_value();
auto pos = hostname.find(':');
if (the_config.value("mtype") != "ipv6" &&
pos != std::string::npos) {
server_port = std::stoi(hostname.substr(pos+1));
hostname.resize(pos);
}
if (report.tls) {
if (int tlsport = srv["tlsport"].int_value()) {
report.measurement_server = hostname;
server_port = tlsport;
break;
}
} else {
report.measurement_server = hostname;
break;
}
}
}
if (report.measurement_server.empty()) {
show_message("Error: no measurement server");
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
return;
}
}
//*out << "Server: " << report.measurement_server << std::endl;
std::string key = arg_obj["hashkey"].string_value();
json11::Json out_args = json11::Json::object {
{ "serverUrl", report.measurement_server },
{ "serverPort", server_port },
{ "userKey", key },
{ "tls", report.tls },
};
if (the_config.value("pingsweep") == "1")
return_msgs.push_back(BridgeTask::msgToAgent("pingSweep"));
else
return_msgs.push_back(BridgeTask::msgToAgent("startTest", out_args.dump()));
} else if (event == "taskProgress") {
std::string tst = arg_obj["task"].string_value();
if (tst == "download" || tst == "upload" || tst == "uploadinfo") {
double val = arg_obj["result"].number_value();
do_output(val, " Mbit/s", false);
}
} else if (event == "taskStart") {
std::string tst = arg_obj["task"].string_value();
if (tst == "download") {
setHeader("Download: ");
} else if (tst == "upload") {
setHeader("Upload: ");
}
} else if (event == "taskComplete") {
std::string tst = arg_obj["task"].string_value();
if (tst == "global") {
if (out_quiet) {
// Enable output:
out->clear(std::istream::goodbit);
if (the_config.value("quiet") != "1")
*out << "\n\nRESULT: ";
std::string limiter = the_config.value("limiter", " ");
*out << report.download << limiter
<< report.upload << limiter
<< report.latency << limiter
<< report.measurement_server << limiter
<< report.isp << limiter
<< report.ticket << limiter
<< measurement_id;
if (report.rating.empty())
*out << std::endl;
else
*out << limiter << report.rating << std::endl;
}
return_msgs.push_back(BridgeTask::msgToAgent("quit"));
} else if (tst == "latency") {
auto res = arg_obj["result"].number_value();
report.latency = res;
if (in_progress_task) {
deferred_latency = true;
} else {
setHeader("Latency: ");
do_output(report.latency, " ms", true);
}
} else if (tst == "download") {
in_progress_task = false;
auto res = arg_obj["result"].number_value();
report.download = res;
do_output(res, " Mbit/s", true);
} else if (tst == "upload") {
in_progress_task = false;
auto res = arg_obj["result"].number_value();
report.upload = res;
do_output(res, " Mbit/s", true);
if (deferred_latency) {
*out << "Latency: " << std::flush;
do_output(report.latency, " ms", true);
}
// Since Measure.AutoSaveReport is false, we tell the agent we're done.
// Any information can be sent in the args object.
return_msgs.push_back(BridgeTask::msgToAgent("saveReport", "{}"));
}
} else if (event == "agentReady") {
// Translate from "user-friendly" option name to agent's name:
static const std::map<std::string, std::string> configMap {
{ "speedlimit", "Measure.SpeedLimit" },
{ "duration", "Measure.LoadDuration" },
{ "iptype", "Measure.IpType" },
{ "hashkey", "Client.hashkey" }
};
std::map<std::string, std::string> newConfig;
if (!savedOptions) {
savedOptions = true;
auto range = the_config.range("configure");
for (auto newp = range.first; newp != range.second; ++newp) {
std::string opt = newp->second;
auto pos = opt.find('=');
std::string attr = opt.substr(0, pos);
auto pp = configMap.find(attr);
if (pp != configMap.end()) {
std::string value = (pos == std::string::npos) ? "1" :
opt.substr(pos+1);
newConfig[pp->second] = value;
}
}
if (!newConfig.empty()) {
json11::Json args = newConfig;
auto mesg = BridgeTask::msgToAgent("saveConfigurationOption",
args.dump());
return_msgs.push_back(mesg);
// Let the agent send agentReady again, with updated options:
return_msgs.push_back(BridgeTask::msgToAgent("clientReady"));
return;
}
}
// Loop over saved configuration options:
for (auto &p : arg_obj.object_items()) {
std::string attr = p.first;
std::string value = p.second.string_value();
if (the_config.value(attr) != "override")
newConfig[attr] = value;
}
if (!newConfig.empty()) {
json11::Json args = newConfig;
auto mesg = BridgeTask::msgToAgent("setConfigurationOption",
args.dump());
return_msgs.push_back(mesg);
}
if (the_config.hasKey("list_measurements")) {
std::map<std::string, std::string> pars;
pars["max"] = the_config.value("list_measurements");
if (the_config.hasKey("list_from"))
pars["from"] = the_config.value("list_from");
// pars["key"] = ...
json11::Json args = json11::Json(pars);
return_msgs.push_back(BridgeTask::msgToAgent("listMeasurements",
args.dump()));
} else {
return_msgs.push_back(BridgeTask::msgToAgent("getConfiguration"));
}
} else if (event == "measurementList") {
std::vector<json11::Json> vec = arg_obj["measurements"].array_items();
if (out_quiet) {
*out << arg_obj.string_value() << std::endl;
} else if (vec.empty()) {
*out << "No measurements found." << std::endl;
} else {
// {"id":83310,"down":113.994,"up":58.6238,"latency":6.28596,"server":"Stockholm","isp":"Stiftelsen for Internetinfrastruktur","ts":1519736433}
*out << "ID\tDownload\tUpload\tLatency\tServer\tISP\tDate\n";
for (auto &m : vec) {
time_t ts = static_cast<time_t>(m["ts"].number_value());
unsigned long id = static_cast<unsigned long>(m["id"].number_value());
if (id && ts)
*out << id << "\t"
<< m["down"].number_value() << "\t"
<< m["up"].number_value() << "\t"
<< m["latency"].number_value() << "\t"
<< m["server"].string_value() << "\t"
<< m["isp"].string_value() << "\t"
<< dateString(ts) << "\n";
}
long n = static_cast<long>(arg_obj["remaining"].number_value());
if (n > 0)
*out << n << " older measurements\n";
}
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
} else if (event == "report") {
auto res = arg_obj["subscription"];
if (res["status"].int_value() != 1)
return;
std::string isp = res["ispOperator"].string_value();
if (!isp.empty() && isp != report.isp)
*out << "Service provider: " << isp << std::endl;
report.msg = res["ispInfoMessage"].string_value();
if (!report.msg.empty())
*out << "Message from service provider: " << report.msg << std::endl;
std::string subs = res["ispSpeedName"].string_value();
if (subs.empty())
return;
*out << "Subscription: " << subs << std::endl;
auto sinfo = arg_obj["subscription_info"];
for (auto &p1 : sinfo.array_items()) {
for (auto &p2 : p1["categories"].array_items()) {
if (p2["description"].string_value() == subs) {
try {
std::string good = p2["good"].string_value(),
acceptable = p2["acceptable"].string_value();
if (good.empty() || acceptable.empty())
return;
if (report.download*1000 >= std::stod(good))
report.rating = "GOOD";
else if (report.download*1000 >= std::stod(acceptable))
report.rating = "ACCEPTABLE";
else
report.rating = "BAD";
*out << "The download result is "
<< report.rating << std::endl;
break;
} catch (...) {
}
}
}
}
if (report.rating == "BAD") {
std::string bmsg = res["ispBadInfoMessage"].string_value();
if (!bmsg.empty() && bmsg != report.msg)
*out << "Message from service provider regarding the download "
<< "result: " << bmsg << std::endl;
}
} else if (event == "measurementInfo") {
measurement_id = arg_obj["MeasurementID"].string_value();
if (!measurement_id.empty())
*out << "Measurement ID: " << measurement_id << std::endl;
std::string imsg = arg_obj["ispInfoMessage"].string_value();
if (!imsg.empty() && imsg != report.msg)
*out << "Message from service provider: " << imsg << std::endl;
} else if (event == "setInfo") {
for (auto &p : arg_obj.object_items()) {
std::string attr = p.first;
std::string value = p.second.string_value();
if (attr == "error") {
if (!value.empty()) {
std::string ecode = arg_obj["errno"].string_value();
if (ecode.empty())
*out << "fatal error: " << value << std::endl;
else
*out << "fatal error: " << value << " (error code "
<< ecode << ")" << std::endl;
break;
}
} else if (attr == "ticket") {
report.ticket = value;
*out << "Support ID: " << value << std::endl;
} else if (attr == "contents") {
json11::Json cobj = p.second;
if (!cobj["consent"].string_value().empty() &&
!cobj["body"].string_value().empty()) {
show_message(cobj["body"].string_value());
show_message("Type Y to accept, N to decline: ", false);
std::string reply;
std::getline(std::cin, reply);
if (reply.find_first_of("Yy") != std::string::npos) {
return_msgs.push_back(BridgeTask::msgToAgent("getConfiguration",
cobj.dump()));
} else {
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
}
} else {
show_message("Server error.");
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
}
} else if (attr == "logText") {
} else if (attr == "approxLatency") {
show_message("Response time: " + value);
} else if (attr == "bestServer") {
if (value.empty())
show_message("error: no server available");
else
show_message("Closest server: " + value);
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
} else if (attr == "msgToUser") {
show_message(value);
}
}
}
}
void CliClient::show_message(const std::string &msg, bool linefeed) {
if (out_quiet)
out->clear(std::istream::goodbit);
*out << msg;
if (linefeed)
*out << std::endl;
else
*out << std::flush;
if (out_quiet)
out->clear(std::istream::eofbit);
}
void CliClient::do_output(double value, const char *msg, bool final) {
if (out_is_tty) // Erase current line:
*out << '\r';
if (out_is_tty || final) {
auto to_delete = current_line.str().size();
current_line.str("");
current_line << current_header << std::setw(10) << std::setprecision(3)
<< std::fixed << value << msg;
*out << current_line.str();
if (to_delete > current_line.str().size())
*out << std::string(to_delete - current_line.str().size(), ' ');
*out << std::flush;
}
if (final) {
current_line.str("");
if (value <= 0)
*out << " test failed";
*out << std::endl;
}
}
void CliClient::do_output(const char *msg, bool final) {
if (out_is_tty) // Erase current line:
*out << '\r';
if (out_is_tty || final) {
auto to_delete = current_line.str().size();
current_line.str("");
current_line << msg;
*out << current_line.str();
if (to_delete > current_line.str().size())
*out << std::string(to_delete - current_line.str().size(), ' ');
*out << std::flush;
}
if (final) {
current_line.str("");
*out << std::endl;
}
}

54
src/cli/cliclient.h Normal file
View file

@ -0,0 +1,54 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <deque>
#include <sstream>
#include "../framework/logger.h"
#include "../framework/synchronousbridge.h"
class CliClient : public Logger, public SynchronousClient {
public:
CliClient(const TaskConfig &config);
void initialMsgToAgent(std::deque<std::string> &return_msgs) override;
// msg is a new message from the agent.
// push any return messages onto return_msgs.
virtual void newEventFromAgent(std::deque<std::string> &return_msgs,
const std::string &msg) override;
void setHeader(const std::string &hdr) {
in_progress_task = true;
*out << hdr << std::flush;
current_header = hdr;
}
private:
// To store the measurement details:
struct {
double latency, download, upload;
std::string ticket, measurement_server, rating, isp, msg;
int tls;
} report = { -1.0, -1.0, -1.0, "no_support_ID", "", "", "", "", 0 };
// Stuff to handle output:
void show_message(const std::string &msg, bool linefeed = true);
void do_output(const char *msg, bool final = false);
void do_output(double value, const char *msg, bool final = false);
std::ostream *out;
bool out_is_tty, out_quiet;
std::ostringstream current_line;
std::string current_header;
std::string measurement_id;
// Set to true during upload and download:
bool in_progress_task = false;
// It latency result arrives asynchronously during
// upload or download, we must wait until the end to show it:
bool deferred_latency = false;
// If user wants to set persistent options, we shoud save them only once.
bool savedOptions = false;
const TaskConfig the_config;
};

56
src/cli/main.cpp Normal file
View file

@ -0,0 +1,56 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <fstream>
#if defined(RUN_SERVER)
#include "../server/measurementserver.h"
#endif
#include "../http/httphost.h"
#include "../http/websocketbridge.h"
#include "../measurement/measurementagent.h"
#include "utils.h"
#include "cliclient.h"
int main(int argc, char *argv[]) {
// Options for the measurement agent and the client (user interface):
TaskConfig agent_cfg, config;
if (!parseArgs(argc, argv, config, agent_cfg))
return 1;
std::ofstream log_file;
config.openlog(log_file);
if (!log_file) {
std::cerr << "cannot write to log file" << std::endl;
return 1;
}
EventLoop loop;
#if defined(RUN_SERVER)
if (config.value("run_server") == "1") {
std::string srv_cfg = "listen " + config.value("listen");
if (!config.value("Measure.LocalAddress").empty())
(srv_cfg += ' ') += config.value("Measure.LocalAddress");
loop.addTask(new MeasurementServer(srv_cfg));
loop.runUntilComplete();
return 0;
}
#endif
CookieFile cf(config.value("config_file"));
HttpHost webserver(agent_cfg.value("Measure.Webserver"), 80, "", 0, &cf);
MeasurementAgent *agent = new MeasurementAgent(agent_cfg, webserver);
CliClient client(config);
if (config.value("listen").empty()) {
loop.addTask(new SynchronousBridge(agent, &client));
} else {
loop.addTask(new WebsocketBridge(agent, config));
}
loop.runUntilComplete();
return 0;
}

325
src/cli/utils.cpp Normal file
View file

@ -0,0 +1,325 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#ifdef _WIN32
#include <windows.h>
#include <direct.h>
#define mkdir(x) _mkdir(x)
#else
#include <csignal>
#include <unistd.h>
#include <sys/stat.h>
#endif
#include <cstdlib>
#include <cerrno>
#include <iostream>
#include <fstream>
#include "utils.h"
#include "../framework/taskconfig.h"
#include "../measurement/defs.h"
#include "../http/sha1.h"
#include "../framework/logger.h"
#include <vector>
namespace {
#ifdef _WIN32
std::string pathSep = "\\";
#else
std::string pathSep = "/";
#endif
}
std::string createAndGetAppDir(std::string dir) {
std::string home;
#ifdef _WIN32
if (dir.empty()) {
if (!std::getenv("HOMEDRIVE") || !std::getenv("HOMEPATH"))
return "";
home = std::string(std::getenv("HOMEDRIVE")) + std::getenv("HOMEPATH");
dir = home + "\\.bredbandskollen";
}
_mkdir(dir.c_str());
#else
if (dir.empty()) {
if (!std::getenv("HOME"))
return "";
home = std::getenv("HOME");
dir = home + "/.bredbandskollen";
}
int status = mkdir(dir.c_str(), 0755);
if (status && errno != EEXIST) {
return "";
}
#endif
#ifdef IS_SANDBOXED
if (!home.empty())
chdir(home.c_str());
#endif
return dir + pathSep;
}
enum class CliMode { NONE, LIVE, TEST, LOCAL,
#if defined(RUN_SERVER)
SERVER,
#endif
IN_ERROR } ;
bool parseArgs(int argc, char *argv[],
TaskConfig &client_cfg, TaskConfig &agent_cfg) {
CliMode mode = CliMode::NONE;
client_cfg.set("port", "80");
client_cfg.set("mtype", "ipv4");
client_cfg.set("listen_addr", "127.0.0.1");
agent_cfg.add("Measure.Webserver", "frontend.bredbandskollen.se");
agent_cfg.add("Measure.SettingsUrl", "/api/servers");
agent_cfg.add("Measure.ContentsUrl", "/api/content");
agent_cfg.add("Measure.MeasurementsUrl", "/api/measurements");
for (int i=1; i<argc; ++i) {
std::string arg(argv[i]);
if (arg == "--v6") {
client_cfg.set("mtype", "ipv6");
client_cfg.set("Measure.IpType", "override");
} else if (arg == "--v4") {
client_cfg.set("mtype", "ipv4");
client_cfg.set("Measure.IpType", "override");
} else if (arg == "--test") {
mode = (mode == CliMode::NONE) ? CliMode::TEST : CliMode::IN_ERROR;
} else if (arg == "--live") {
mode = (mode == CliMode::NONE) ? CliMode::LIVE : CliMode::IN_ERROR;
} else if (arg == "--version") {
std::cout << measurement::appName << ' '
<< measurement::appVersion << '\n';
return false;
} else if (arg == "--quiet") {
client_cfg.set("quiet", "1");
} else if (arg == "--csv") {
client_cfg.set("quiet", "1");
client_cfg.set("limiter", ",");
} else if (arg == "--local") {
mode = (mode == CliMode::NONE) ? CliMode::LOCAL : CliMode::IN_ERROR;
#if defined(RUN_SERVER)
} else if (arg == "--run-server") {
mode = (mode == CliMode::NONE) ? CliMode::SERVER : CliMode::IN_ERROR;
#endif
} else if (arg.substr(0, 11) == "--duration=") {
agent_cfg.set("Measure.LoadDuration", argv[i]+11);
client_cfg.set("Measure.LoadDuration", "override");
} else if (arg.substr(0, 13) == "--speedlimit=") {
agent_cfg.set("Measure.SpeedLimit", argv[i]+13);
client_cfg.set("Measure.SpeedLimit", "override");
} else if (arg.substr(0, 6) == "--out=")
client_cfg.set("out", argv[i]+6);
else if (arg.substr(0, 6) == "--dir=")
client_cfg.set("app_dir", (argv[i]+6) + pathSep);
else if (arg.substr(0, 6) == "--log=")
client_cfg.set("logfile", argv[i]+6);
else if (arg.substr(0, 11) == "--local-ip=")
agent_cfg.set("Measure.LocalAddress", argv[i]+11);
else if (arg.substr(0, 9) == "--server=")
client_cfg.set("server", argv[i]+9);
else if (arg.substr(0, 7) == "--port=")
client_cfg.set("port", argv[i]+7);
else if (arg.substr(0, 12) == "--configure=")
client_cfg.add("configure", argv[i]+12);
else if (arg.substr(0, 9) == "--listen=")
client_cfg.set("listen", argv[i]+9);
else if (arg.substr(0, 14) == "--listen-addr=")
client_cfg.set("listen_addr", argv[i]+14);
else if (arg.substr(0, 12) == "--listen-pw=") {
client_cfg.set("listen_pw", argv[i]+12);
#ifdef USE_GNUTLS
} else if (arg == "--ssl") {
agent_cfg.set("Measure.TLS", "1");
client_cfg.set("ssl", "1");
if (client_cfg.value("port") == "80")
client_cfg.set("port", "443");
#endif
} else if (arg.substr(0, 9) == "--fakeip=")
agent_cfg.set("Client.fakeip", argv[i]+9);
else if (arg == "--check-servers")
client_cfg.set("pingsweep", "1");
else if (arg.substr(0, 14) == "--measurements")
client_cfg.set("list_measurements",
(arg.size() > 15 && arg[14] == '=') ? argv[i]+15 : "10");
else if (arg.substr(0, 10) == "--from-id=") {
client_cfg.set("list_from", argv[i]+10);
if (client_cfg.value("list_measurements").empty())
client_cfg.set("list_measurements", "10");
} else if (arg == "--browser") {
client_cfg.set("browser", "1");
if (client_cfg.value("listen").empty())
client_cfg.set("listen", "0"); // Use any avaliable port
} else if (arg.substr(0, 13) == "--proxy-host=")
agent_cfg.set("Measure.ProxyServerUrl", argv[i]+13);
else if (arg.substr(0, 13) == "--proxy-port=")
agent_cfg.set("Measure.ProxyServerPort", argv[i]+13);
else {
int status = 0;
if (arg != "--help") {
status = 1;
std::cerr << argv[0] << ": invalid argument -- " << arg << std::endl;
}
std::ostream &fh = status ? std::cerr : std::cout;
fh << "Usage: " << argv[0] << " [OPTION]...\n\nOptions:\n\n"
" --help Show this help text\n"
" --version Print version number and exit\n"
<< "\nNetwork related options:\n"
#ifndef BBK_WEBVIEW
<< " --v6 Prefer IPv6 (default is IPv4)\n"
#endif
#ifdef __linux__
#else
<< " --local-ip=IP Measure using existing local ip address IP\n"
<< " Note: this will not work on all platforms\n"
#endif
<< " --proxy-host=HOST Use HTTP proxy server HOST\n"
<< " --proxy-port=PORT Use port PORT on proxy server (default 80)\n"
<< "\nMeasurement configuration:\n"
#ifndef BBK_WEBVIEW
<< " --server=HOST Use HOST as measurement server\n"
<< " --port=N Port number for measurement server, default 80\n"
#endif
#ifdef USE_GNUTLS
<< " --ssl Measure using transport layer security (default port 443)\n"
#endif
<< " --duration=N Measure upload/download for N seconds (2-10, default 10)\n"
<< " --speedlimit=N Keep upload/download speed below N mbps on average\n"
<< "\nMeasurement type:\n"
<< " --live Measure using Bredbandskollen's live servers (default)\n"
<< " --test Measure using Bredbandskollen's development servers\n"
#ifndef BBK_WEBVIEW
<< " --local Don't fetch configuration (server list) from bredbandskollen.se,\n"
<< " communicate only with server given by the --server option.\n"
#endif
#if defined(RUN_SERVER)
<< " --run-server Run as a measurement server (requires option --listen=PORT)\n"
#endif
<< "\nLogging:\n"
<< " --log=FILENAME Write debug log to FILENAME\n"
<< " (log to stderr if FILENAME is -)\n"
#ifndef BBK_WEBVIEW
<< "\nFinding measurement servers:\n"
<< " --check-servers Find closest measurement server\n"
<< "\nList previous measurements:\n"
<< " --measurements List 10 last measurements\n"
<< " --measurements=N List N last measurements\n"
<< " If --quiet, output will be JSON. Otherwise\n"
<< " output will be lines with tab separated fields.\n"
<< " --from-id=N List only measurements before ID N\n"
<< "\nBrowser interface:\n"
<< " --browser Use a web browser as interface\n"
<< " --listen=PORT Use web browser as interface;\n"
<< " the browser must connect to the given PORT\n"
<< " --listen-addr=IP When listening, bind socket to ip address IP\n"
<< " (default is 127.0.0.1) to use a web browser on\n"
<< " a remote host as interface\n"
<< " Note: this may not work due to e.g. firewalls.\n"
<< " Don't use it unless you know what you are doing.\n"
<< " --listen-pw=PW Use PW as a one-time password when connecting from browser\n"
<< " Note: DO NOT reuse a sensitive password here!\n"
<< " It is better to omit this option because by default\n"
<< " a secure one-time password will be generated.\n"
<< "\nCommand line interface:\n"
<< " --quiet Write a single line of output\n"
<< " --csv Write a single line of output, comma separated\n"
<< " --out=FILENAME Append output to FILENAME instead of stdout\n"
#endif
<< std::endl;
return false;
}
}
client_cfg.set("app_dir", createAndGetAppDir(client_cfg.value("app_dir")));
if (client_cfg.value("local") == "1" && client_cfg.value("server").empty()) {
std::cerr << "missing --server option" << std::endl;
return false;
}
std::vector<std::string> pdir = { "listen", "port" };
for (auto &str : pdir)
if (!client_cfg.value(str).empty()) {
auto port = client_cfg.value(str);
if (port.find_first_not_of("0123456789") != std::string::npos ||
port.size() > 5 || std::stod(port) > 65535) {
std::cerr << "invalid port number" << std::endl;
return false;
}
}
switch (mode) {
case CliMode::NONE:
case CliMode::LIVE:
agent_cfg.set("Measure.Webserver", "frontend.bredbandskollen.se");
break;
case CliMode::TEST:
agent_cfg.set("Measure.Webserver", "frontend-beta.bredbandskollen.se");
break;
case CliMode::LOCAL:
client_cfg.set("local", "1");
agent_cfg.set("Measure.Webserver", "none");
break;
#if defined(RUN_SERVER)
case CliMode::SERVER:
client_cfg.set("local", "1");
client_cfg.set("run_server", "1");
if (client_cfg.value("listen").empty()) {
std::cerr << "option --listen is required with --run-server"
<< std::endl;
return false;
}
break;
#endif
case CliMode::IN_ERROR:
std::cerr << "can have only one of options --live, --test,";
#if defined(RUN_SERVER)
std::cerr << " --run-server,";
#endif
std::cerr << " and --local";
return false;
}
if (!client_cfg.value("listen").empty() &&
client_cfg.value("listen_pw").empty()) {
client_cfg.add("listen_pw", Logger::createHashKey(12));
}
client_cfg.add("url", "http://" + agent_cfg.value("Measure.Webserver") +
"/standalone/dev/index.php");
if (client_cfg.value("logfile").empty()) {
#if defined(RUN_SERVER)
if (mode == CliMode::SERVER)
client_cfg.add("logfile", client_cfg.value("app_dir") + "server_log");
else
#endif
client_cfg.add("logfile", client_cfg.value("app_dir") + "last_log");
}
client_cfg.set("config_file", client_cfg.value("app_dir") + "config");
agent_cfg.set("options_file", client_cfg.value("app_dir") + "ConfigOptions.txt");
// Default to ipv6 if user wants to use a local ipv6 address
if (agent_cfg.value("Measure.LocalAddress").find(':') !=
std::string::npos) {
client_cfg.set("mtype", "ipv6");
client_cfg.set("Measure.IpType", "override");
}
agent_cfg.add("Measure.AutoSaveReport",
client_cfg.value("listen").empty() ? "false" : "true");
agent_cfg.add("Measure.IpType", client_cfg.value("mtype"));
agent_cfg.add("Client.appname", measurement::appName);
agent_cfg.add("Client.appver", measurement::appVersion);
agent_cfg.add("Client.machine", measurement::hw_info);
agent_cfg.add("Client.system", measurement::os_version);
agent_cfg.add("Client.language", "en");
return true;
}

11
src/cli/utils.h Normal file
View file

@ -0,0 +1,11 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
class TaskConfig;
std::string createAndGetAppDir(std::string dir = "");
bool parseArgs(int argc, char *argv[],
TaskConfig &client_cfg, TaskConfig &agent_cfg);

10
src/examples/.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
boss
cliclient
client
echoclient
echoserver
threadserver
timers
webserver
winner
winnerclient

View file

@ -0,0 +1,25 @@
# Name of the executable program to be built:
TARGET = client
# Relative path to where the framework directory is located:
DIRLEVEL = ../..
# Possible LOGLEVEL values: dbg, info, warn, err, none
LOGLEVEL = dbg
# Set to 1 to link with GnuTLS, i.e. to enable SSL support.
GNUTLS = 0
# Set to 1 to be able to run tasks in different threads.
THREADS = 0
# All C++ source files used by the target program
SOURCES = main.cpp
# To be able to debug the target program
CXXFLAGS += -g
# Include the HTTP support files. It will also include the base framework,
# i.e. $(DIRLEVEL)/framework/mk.inc
# The below line should be the last one in the Makefile.
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,18 @@
#include <http/singlerequest.h>
#include <framework/eventloop.h>
/* Example 001
Fetch http://frontend.bredbandskollen.se/api/servers, then exit.
Write debug log to stderr.
*/
int main(int , char *[]) {
Task *t = new SingleRequest("MyRequest",
"frontend.bredbandskollen.se",
"/api/servers");
EventLoop::runTask(t);
// Note: the object t will be deleted by the event loop.
return 0;
}

View file

@ -0,0 +1,25 @@
# Name of the executable program to be built:
TARGET = client
# Relative path to where the framework directory is located:
DIRLEVEL = ../..
# Possible LOGLEVEL values: dbg, info, warn, err, none
LOGLEVEL = dbg
# Set to 1 to link with GnuTLS, i.e. to enable SSL support.
GNUTLS = 0
# All C++ source files used by the target program
SOURCES = main.cpp
# To be able to debug the target program
CXXFLAGS += -g
# Extra files to be removed by "make clean":
CLEAN += log.txt
# Include the HTTP support files. It will also include the base framework,
# i.e. $(DIRLEVEL)/framework/mk.inc
# The below line should be the last one in the Makefile.
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,77 @@
#include <http/singlerequest.h>
#include <framework/eventloop.h>
/* Example 002
Fetch http://frontend.bredbandskollen.se/api/servers.
Write the result to stdout.
Write debug log to log.txt.
*/
class MainTask : public Task {
public:
// Each task object has a name (or "label"), which is used (for example)
// by the logger.
MainTask(const std::string &name) : Task(name) {
// Note: the constructor will be executed before the task has been added
// to the eventloop.
// DO NOT perform any actions that concern the eventloop here!
// Instead, most of the initialisation should be performed from within
// the start() method, which will be called by the eventloop when the
// execution of this task starts.
}
double start() override {
dbg_log() << "starting";
// If child tasks still exist when this task is done,
// we want them to be removed:
killChildTaskWhenFinished();
// The second parameter to addNewTask sets this task as
// parent of the new task.
addNewTask(new SingleRequest("MyRequest",
"frontend.bredbandskollen.se",
"/api/servers"), this);
// First timer is to be called after 10.0 seconds.
// If we return <= 0, no timer will be added.
return 10.0;
}
double timerEvent() override {
std::cerr << "Timeout after " << elapsed() << " seconds";
// Tell the eventloop that this task has given up:
setTimeout();
// Return number of seconds until this method should be
// called again, or <= 0 if you don't want it to be called again.
return 0;
}
// Will be called when a child task is finished:
void taskFinished(Task *task) override {
log() << task->label() << " finished, ok=" << task->finishedOK();
// Our only child task is a SingleRequest task, so the below cast will
// succeed. We need the cast to be able to call httpStatus().
if (SingleRequest *req = dynamic_cast<SingleRequest *>(task)) {
log() << "Status: " << req->httpStatus();
std::cout << "Result: " << req->result() << std::endl;
// Calling setResult tells the eventloop that this task is done.
setResult("OK");
}
}
};
int main(int , char *[]) {
std::ofstream log("log.txt");
// Note: the log file object must not be destroyed until either the
// eventloop is finished, or until setLogFile is called again.
Logger::setLogFile(log);
EventLoop::runTask(new MainTask("Main Task"));
return 0;
}

View file

@ -0,0 +1,7 @@
TARGET = webserver
DIRLEVEL = ../..
LOGLEVEL = info
GNUTLS = 0
SOURCES = main.cpp
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,55 @@
#include <http/webservertask.h>
#include <framework/eventloop.h>
/* Example 003
Start a webserver on port 8080. Write log to stderr.
From a web browser, you can retrieve the URLs
http://127.0.0.1:8080/getTime
http://127.0.0.1:8080/getStats
http://127.0.0.1:8080/shutdown
*/
class WebServer : public WebServerTask {
public:
WebServer(const std::string &cfg) :
WebServerTask("WebServer", cfg) {
}
HttpState newGetRequest(HttpServerConnection *,
const std::string &uri) override;
private:
unsigned long tot_no_requests = 0;
};
HttpState WebServer::newGetRequest(HttpServerConnection *conn,
const std::string &uri) {
++tot_no_requests;
log() << "URI: " << uri << " #" << tot_no_requests;
if (uri == "/getTime")
// Send current time
conn->sendHttpResponse(headers("200 OK"), "text/plain", dateString());
else if (uri == "/getStats")
// Send number of requests since server was started.
conn->sendHttpResponse(headers("200 OK"), "text/plain",
std::to_string(tot_no_requests));
else if (uri == "/shutdown") {
conn->sendHttpResponse(headers("200 OK"), "text/plain",
"server shutdown");
// This will terminate the task, i.e. shut the server down. Normally, of
// course, clients wouldn't be able to do this on a production server.
setResult("");
} else
conn->sendHttpResponse(headers("404 Not Found"), "text/plain",
"unknown service");
return HttpState::WAITING_FOR_REQUEST;
}
int main(int , char *[]) {
// Listen on port 8080. To listen on the standard port 80, we'd have to
// run as a privileged user.
EventLoop::runTask(new WebServer("listen 8080"));
return 0;
}

View file

@ -0,0 +1,22 @@
# Name of the executable program to be built:
TARGET = timers
# Relative path to where the framework directory is located:
DIRLEVEL = ../..
# Possible LOGLEVEL values: dbg, info, warn, err, none
LOGLEVEL = dbg
# Set to 1 to link with GnuTLS, i.e. to enable SSL support.
GNUTLS = 0
# All C++ source files used by the target program
SOURCES = main.cpp
# To be able to debug the target program
CXXFLAGS += -g
# Include the HTTP support files. It will also include the base framework,
# i.e. $(DIRLEVEL)/framework/mk.inc
# The below line should be the last one in the Makefile.
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,45 @@
#include <framework/task.h>
#include <framework/eventloop.h>
/* Example 004
Simple demonstration of tasks and timers.
Write log to stderr.
*/
class PointlessTask : public Task {
public:
PointlessTask(const std::string &name,
double tick_length, unsigned int no_ticks) :
Task(name),
tick_duration(tick_length),
ticks(no_ticks), curr_tick(0) {
}
double start() override {
dbg_log() << "starting, " << ticks << " ticks";
return tick_duration;
}
double timerEvent() override {
log() << "timerEvent " << ++curr_tick << " of " << ticks;
if (curr_tick < ticks)
return tick_duration;
setResult("Done after " + std::to_string(elapsed()) + " seconds");
return 0.0;
}
private:
double tick_duration;
unsigned int ticks, curr_tick;
};
int main(int , char *[]) {
EventLoop loop;
loop.addTask(new PointlessTask("Pointless 1", 1.0, 3));
loop.addTask(new PointlessTask("Pointless 2", 0.7, 5));
loop.addTask(new PointlessTask("Pointless 3", 0.5, 7));
loop.runUntilComplete();
return 0;
}

View file

@ -0,0 +1,7 @@
TARGET = winner
DIRLEVEL = ../..
LOGLEVEL = info
GNUTLS = 0
SOURCES = main.cpp winner.cpp
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,36 @@
#include <framework/eventloop.h>
#include "winner.h"
/* Example 005
Start a webserver on port 8080. Write log to stderr.
The client can open a websocket connection on
ws://127.0.0.1:8080/winner
The client can then send text messages of the format
name score
and the server will remember the names and scores.
If the client sends the text message "winner", the
server will respond with the name with the highest score.
E.g. the client sends the five messages
Bill 12
Steve 19
Linus 33
Ken 27
winner
and the server responds
Linus
*/
int main(int , char *[]) {
EventLoop::runTask(new Winner("listen 8080"));
return 0;
}

View file

@ -0,0 +1,43 @@
#include "winner.h"
bool Winner::newWsRequest(HttpServerConnection *,
const std::string &uri) {
log() << "websocket request to " << uri;
if (uri == "/winner")
return true;
else
return false;
}
bool Winner::wsTextMessage(HttpConnection *conn,
const std::string &msg) {
log() << "Got: " << msg;
auto p = leader.find(conn);
if (msg == "winner") {
if (p != leader.end()) {
log() << "Sending " << p->second.name;
conn->sendWsMessage(p->second.name);
}
return true;
}
auto pos = msg.find_last_of(" ");
if (pos == std::string::npos || !pos || pos+1 == msg.size())
return true;
if (msg.find_last_not_of("0123456789") != pos)
return true;
auto score = std::stoul(msg.substr(pos+1));
if (p == leader.end()) {
leader[conn] = { msg.substr(0, pos), score };
} else if (score > p->second.score) {
p->second.score = score;
p->second.name = msg.substr(0, pos);
} else if (score == p->second.score) {
(p->second.name += ", ") += msg.substr(0, pos);
}
return true;
}
void Winner::connRemoved(SocketConnection *conn) {
leader.erase(dynamic_cast<HttpConnection *>(conn));
}

View file

@ -0,0 +1,30 @@
#pragma once
#include <http/webservertask.h>
#include <string>
#include <map>
struct Info {
std::string name;
unsigned long score;
};
class Winner : public WebServerTask {
public:
Winner(const std::string &cfg) :
WebServerTask("Winner", cfg) {
}
// Client want's to open a websocket connection on uri.
// Return true to accept, false to close the connection.
bool newWsRequest(HttpServerConnection *conn,
const std::string &uri) override;
// When a text message is sent from the client, we'll be notified here:
bool wsTextMessage(HttpConnection *conn,
const std::string &msg) override;
void connRemoved(SocketConnection *conn) override;
private:
std::map<HttpConnection *, Info> leader;
};

View file

@ -0,0 +1,7 @@
TARGET = winnerclient
DIRLEVEL = ../..
LOGLEVEL = info
GNUTLS = 0
SOURCES = main.cpp winnerclient.cpp
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,13 @@
#include <framework/eventloop.h>
#include "winnerclient.h"
/* Example 006
A websocket client talking to the server of example 005.
*/
int main(int , char *[]) {
EventLoop::runTask(new WinnerClient("127.0.0.1", 8080, "/winner"));
return 0;
}

View file

@ -0,0 +1,37 @@
#include <framework/eventloop.h>
#include "winnerclient.h"
/* Example 006
A websocket client talking to the server of example 005.
This is an alternative version of the main function, where you can start the
client with the command line parameters
--logfile=
--host=
--port=
--url=
To use this version, update the Makefile to use main2.cpp instead of main.cpp.
*/
int main(int argc, char *argv[]) {
TaskConfig cfg;
cfg.set("host", "127.0.0.1");
cfg.set("port", "8080");
cfg.set("url", "/winner");
cfg.parseArgs(argc, argv);
// Use value of parameter --logfile as filename to save the log if it exists
// and isn't equal to "-".
std::ofstream log;
cfg.openlog(log);
auto task = new WinnerClient(cfg.value("host"),
std::stoi(cfg.value("port")),
cfg.value("url"));
EventLoop::runTask(task);
return 0;
}

View file

@ -0,0 +1,49 @@
#include "winnerclient.h"
WinnerClient::WinnerClient(const std::string &host, uint16_t port,
const std::string &url) :
HttpClientTask("WinnerClient", HttpHost(host, port)),
_url(url) {
}
double WinnerClient::start() {
log() << "starting";
if (!createNewConnection()) {
log() << "Cannot connect to server\n";
setResult("error");
}
return 10;
}
void WinnerClient::connRemoved(SocketConnection *) {
if (!terminated()) {
log() << "Lost connection";
setResult("error");
}
}
void WinnerClient::newRequest(HttpClientConnection *conn) {
conn->ws_get(_url);
}
bool WinnerClient::requestComplete(HttpClientConnection *) {
// Server ignored websocket upgrade request
return false;
}
bool WinnerClient::websocketUpgrade(HttpClientConnection *conn) {
log() << "connected";
conn->sendWsMessage("Bill 12");
conn->sendWsMessage("Steve 19");
conn->sendWsMessage("Linus 33");
conn->sendWsMessage("Ken 27");
conn->sendWsMessage("winner");
return true;
}
bool WinnerClient::wsTextMessage(HttpConnection *,
const std::string &msg) {
log() << "Got " << msg;
setResult(msg);
return false;
}

View file

@ -0,0 +1,18 @@
#pragma once
#include <http/httpclienttask.h>
class WinnerClient : public HttpClientTask {
public:
WinnerClient(const std::string &host, uint16_t port,
const std::string &url);
double start() override;
void connRemoved(SocketConnection *conn) override;
void newRequest(HttpClientConnection *conn) override;
bool requestComplete(HttpClientConnection *) override;
bool websocketUpgrade(HttpClientConnection *) override;
bool wsTextMessage(HttpConnection *conn,
const std::string &msg) override;
private:
std::string _url;
};

View file

@ -0,0 +1,10 @@
TARGET = client
DIRLEVEL = ../..
LOGLEVEL = dbg
GNUTLS = 0
SOURCES = main.cpp
CXXFLAGS += -g
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,78 @@
#include <framework/task.h>
#include <http/singlerequest.h>
#include <framework/eventloop.h>
/* Example 007
Simple demonstration of sending events (direct messages) between tasks.
The SenderTask will send a message to ReceiverTask each second.
The ReceiverTask will terminate after the third message.
The SenderTask will terminate after the ReceiverTask has terminated.
*/
class ReceiverTask;
class SenderTask : public Task {
public:
SenderTask(ReceiverTask *task) : Task("SenderTask"), peer(task) {
}
double start() override;
double timerEvent() override;
void taskFinished(Task *task) override;
private:
ReceiverTask *peer;
};
class ReceiverTask : public Task {
public:
ReceiverTask() : Task("ReceiverTask") {
}
void handleExecution(Task *, const std::string &msg) override;
private:
unsigned int msgCount = 0;
};
void ReceiverTask::handleExecution(Task *, const std::string &msg) {
log() << "Event: " << msg;
if (++msgCount == 3)
setResult("Got Event");
}
double SenderTask::start() {
// We have to register an "interest" in peer by calling the startObserving
// method, otherwise the messages we try to send to it will be ignored.
// Also, by doing this we will be notified (through taskFinished) if
// the peer task dies; that is important since we must not keep the pointer
// to the peer after it has been deleted.
if (!startObserving(peer)) {
log() << "Peer task does not exist.";
setResult("Fail");
return 0.0;
}
return 0.1;
}
double SenderTask::timerEvent() {
executeHandler(peer, "Hi there!");
return 1.0;
}
void SenderTask::taskFinished(Task *task) {
if (task == peer) {
peer = nullptr;
log() << "Peer task dead, will exit.";
setResult("Done.");
}
}
int main(int , char *[]) {
EventLoop eventloop("EventLoop");
ReceiverTask *it = new ReceiverTask();
eventloop.addTask(it);
eventloop.addTask(new SenderTask(it));
eventloop.runUntilComplete();
return 0;
}

View file

@ -0,0 +1,11 @@
TARGET = boss
DIRLEVEL = ../..
LOGLEVEL = dbg
GNUTLS = 0
SOURCES = main.cpp
CLEAN += w0.log w1.log w2.log
CXXFLAGS += -g
include $(DIRLEVEL)/framework/mk.inc

View file

@ -0,0 +1,103 @@
#include <stdlib.h>
#include <unistd.h>
#include <fstream>
#include <framework/task.h>
#include <framework/socketreceiver.h>
/* Example 008
Demonstration of a task that creates workers (subprocesses).
The Boss task creates three subprocesses, each running a Worker task.
Each Worker will do some processing and then exit.
The Boss will exit when all worker processes are done.
This example does not work on Windows.
*/
class Worker : public Task {
public:
Worker(const std::string label) : Task(label) {
}
~Worker() override {
log() << "Goodbye from " << label();
}
double start() override;
private:
};
double Worker::start() {
log() << "Worker PID " << getpid() << " started.";
srand(static_cast<unsigned int>(getpid()));
unsigned long count = 0;
while (true) {
int n = rand();
++count;
if (n % 10000000 == 3333333) {
setResult(std::to_string(count) + " tries, result " +
std::to_string(n));
break;
}
}
return 0.0;
}
class Boss : public Task {
public:
Boss(unsigned int no_workers) :
Task("Boss"),
tot_no_workers(no_workers) {
}
~Boss() override {
}
// After creating a child process, the eventloop will call our
// createWorkerTask method to get a task to run in the new process.
Task *createWorkerTask(unsigned int wno) override {
std::string name = "w" + std::to_string(wno);
log() << "Will create worker " << name;
return new Worker(name);
}
double start() override;
void processFinished(int pid, int wstatus) override {
log() << "OK, end of " << pid << " status " << wstatus;
if (++wdone == tot_no_workers)
setResult("all done");
}
private:
const unsigned int tot_no_workers;
unsigned int wdone = 0;
};
double Boss::start() {
// Messages and sockets can be passed between parent and worker processes
// through "channels". We don't use channels in this example.
// The createWorker call instructs the eventloop to create a child process
// and run a new eventloop in that process. Our createWorkerTask method
// will be called in the child process to provide a task for the eventloop
// running in the child process.
// First parameter to createWorker is a file name for the worker's log.
// The second parameter is the number of channels between parent and worker.
for (unsigned int n=0; n < tot_no_workers; ++n)
createWorker("w" + std::to_string(n) + ".log", 0);
return 0;
}
int main(int , char *[]) {
EventLoop eventloop("EventLoop");
eventloop.addTask(new Boss(3));
eventloop.runUntilComplete();
return 0;
}

View file

@ -0,0 +1,12 @@
TARGET = threadserver
DIRLEVEL = ../..
LOGLEVEL = dbg
THREADS = 1
GNUTLS = 0
SOURCES = main.cpp
CLEAN += main.log M8000.log M8080.log
CXXFLAGS += -g
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,63 @@
#include <stdlib.h>
#include <unistd.h>
#include <fstream>
#include <algorithm>
#include <http/webservertask.h>
#include <framework/eventloop.h>
/* Example 009
Create two threads, each running a WebServer in its own eventloop.
One will listen on port 8000, the other on 8080. To test, open URLs
http://127.0.0.1:8000/getTime
http://127.0.0.1:8080/getTime
http://127.0.0.1:8000/getStats
http://127.0.0.1:8080/getStats
http://127.0.0.1:8000/stop
http://127.0.0.1:8080/stop
in a web browser. The /stop URL will tell each server to exit. The test program
will exit when both servers have been stopped.
*/
class WebServer : public WebServerTask {
public:
WebServer(const std::string &cfg, const std::string &name) :
WebServerTask(name, cfg) {
}
HttpState newGetRequest(HttpServerConnection *,
const std::string &uri) override;
private:
unsigned long tot_no_requests = 0;
};
HttpState WebServer::newGetRequest(HttpServerConnection *conn,
const std::string &uri) {
++tot_no_requests;
log() << "URI: " << uri << " #" << tot_no_requests;
if (uri == "/getTime")
conn->sendHttpResponse(headers("200 OK"), "text/plain", dateString());
else if (uri == "/getStats")
conn->sendHttpResponse(headers("200 OK"), "text/plain",
std::to_string(tot_no_requests));
else if (uri == "/stop")
setResult("DONE");
else
conn->sendHttpResponse(headers("404 Not Found"), "text/plain",
"unknown service");
return HttpState::WAITING_FOR_REQUEST;
}
int main(int , char *[]) {
std::ofstream my_log("main.log"), log1("M8000.log"), log2("M8080.log");
Logger::setLogFile(my_log);
EventLoop loop;
loop.spawnThread(new WebServer("listen 8000", "W8000"), "M8000", &log1);
loop.spawnThread(new WebServer("listen 8080", "W8080"), "M8080", &log2);
loop.runUntilComplete();
return 0;
}

View file

@ -0,0 +1,10 @@
TARGET = echoserver
DIRLEVEL = ../..
LOGLEVEL = dbg
GNUTLS = 0
SOURCES = echoserverconnection.cpp \
echoservertask.cpp \
echoserver.cpp
include $(DIRLEVEL)/framework/mk.inc

View file

@ -0,0 +1,23 @@
#include <framework/eventloop.h>
#include "echoservertask.h"
/* Example 010
Implement a server for the echo network protocol, i.e.
just echo all received data back to the client.
Write log to stderr.
The class EchoServerTask takes a port number as a parameter.
Will accept clients on that port number.
To test it, start the server and open one or more other terminals.
In each of the terminals, run the command
telnet 127.0.0.1 1400
and type a few lines of text. The server should echo back the text.
*/
int main(int , char *[]) {
EventLoop::runTask(new EchoServerTask(1400));
return 0;
}

View file

@ -0,0 +1,39 @@
#include "echoserverconnection.h"
EchoServerConnection::EchoServerConnection(Task *task, int fd,
const char *ip, uint16_t port) :
SocketConnection("Echo Client Handler", task, fd, ip, port) {
}
PollState EchoServerConnection::connected() {
log() << "New client " << id() << ", waiting for data";
return PollState::READ;
}
PollState EchoServerConnection::readData(char *buf, size_t len) {
// Just send the data back to the client
asyncSendData(buf, len);
if (len > 20) {
dbg_log() << "Client " << id() << " said "
<< std::string(buf, 20) << "... (" << len << " bytes)";
} else {
dbg_log() << "Client " << id() << " said " << std::string(buf, len);
}
if (asyncBufferSize() > 1000000) {
// The client does not read data as fast as we receive it.
// We have to stop reading until the outgoing buffer
// has shrunk to a manageable size.
dbg_log() << "Send buffer too large: " << asyncBufferSize();
return PollState::READ_BLOCKED;
}
return PollState::READ;
}
PollState EchoServerConnection::checkReadBlock() {
dbg_log() << "Check send buffer: " << asyncBufferSize();
if (asyncBufferSize() > 100000)
return PollState::READ_BLOCKED;
return PollState::READ;
}

View file

@ -0,0 +1,19 @@
#pragma once
#include <framework/socketconnection.h>
class EchoServerConnection : public SocketConnection {
public:
EchoServerConnection(Task *task, int fd, const char *ip, uint16_t port);
// Called by the eventloop when a client connection has been established.
PollState connected() override;
// Called by the eventloop when data has arrived from the client.
PollState readData(char *buf, size_t len) override;
// Called regularly by the eventloop if we have blocked incoming data,
// i.e. if we have returned PollState::READ_BLOCKED from the above method.
PollState checkReadBlock() override;
private:
};

View file

@ -0,0 +1,52 @@
#include "echoservertask.h"
#include "echoserverconnection.h"
#include <framework/serversocket.h>
EchoServerTask::EchoServerTask(uint16_t port) :
Task("Echo Server"),
port_number(port) {
}
double EchoServerTask::start() {
// The ServerSocket object is owned by the eventloop and will be deleted
// by the eventloop when it's no longer needed.
if (addServer(new ServerSocket("Echo Listen Socket", this, port_number)))
return 2.0;
setResult("Failed, cannot start server");
return 0;
}
double EchoServerTask::timerEvent() {
log() << "Current number of clients: " << no_clients;
return 5.0;
}
SocketConnection *EchoServerTask::newClient(int fd, const char *ip,
uint16_t port, ServerSocket *) {
// The EchoServerConnection object is owned by the eventloop and will be
// deleted by the eventloop when it's no longer needed.
// Just before deleting it, the eventloop will notify us by calling the
// below connRemoved method.
// We really shoudn't keep local copies of the (pointers to the) connection
// objects, but if we still do so, we _must_ implement connRemoved to delete
// our local copies so we don't use them after the objects are deleted.
return new EchoServerConnection(this, fd, ip, port);
}
void EchoServerTask::connAdded(SocketConnection *) {
log() << "added client";
++no_clients;
}
void EchoServerTask::connRemoved(SocketConnection *) {
log() << "lost client";
--no_clients;
}
void EchoServerTask::serverAdded(ServerSocket *) {
log() << "listen socket added";
}
void EchoServerTask::serverRemoved(ServerSocket *) {
setResult("Server hangup!");
}

View file

@ -0,0 +1,39 @@
#pragma once
#include <framework/task.h>
class EchoServerTask : public Task {
public:
EchoServerTask(uint16_t port);
double start() override;
double timerEvent() override;
// Whenever a new client connects to our server socket, the eventloop
// will call the below method. To reject the connection, we could return
// nullptr. If we accept the connection we must create and return an object
// belonging to a subclass of SocketConnection.
SocketConnection *newClient(int fd, const char *ip,
uint16_t port, ServerSocket *) override;
// The eventloop will call this method to notify us each time a new
// SocketConnection has been added, i.e. after the newClient call.
void connAdded(SocketConnection *conn) override;
// The eventloop will call this method to notify us each time an existing
// SocketConnection has been removed.
void connRemoved(SocketConnection *conn) override;
// The eventloop will call this method to notify us each time a new
// server (listening) socket has been added.
void serverAdded(ServerSocket *conn) override;
// The eventloop will call this method to notify us each time an existing
// server (listening) socket has been removed.
void serverRemoved(ServerSocket *conn) override;
private:
uint16_t port_number;
unsigned int no_clients = 0;
};

View file

@ -0,0 +1,10 @@
TARGET = echoclient
DIRLEVEL = ../..
LOGLEVEL = dbg
GNUTLS = 0
SOURCES = echoclientconnection.cpp \
echoclienttask.cpp \
echoclient.cpp
include $(DIRLEVEL)/framework/mk.inc

View file

@ -0,0 +1,14 @@
#include <framework/eventloop.h>
#include "echoclienttask.h"
/* Example 011
Write a few messages to an echo server, check the result.
Make sure the server from example 010 is running while you run this client.
*/
int main(int , char *[]) {
EventLoop::runTask(new EchoClientTask("127.0.0.1", 1400));
return 0;
}

View file

@ -0,0 +1,36 @@
#include "echoclientconnection.h"
#include "echoclienttask.h"
EchoClientConnection::
EchoClientConnection(EchoClientTask *task, const std::string &hostname,
unsigned int port) :
SocketConnection("EchoClientConnection", task, hostname, port) {
}
PollState EchoClientConnection::connected() {
msg_out = "Hi ho, hi ho";
log() << "Socket connected, sending " << msg_out;
asyncSendData(msg_out);
return PollState::READ;
}
PollState EchoClientConnection::readData(char *buf, size_t len) {
log() << "Got data: " << std::string(buf, len);
msg_in.append(buf, len);
if (msg_in.size() == msg_out.size()) {
if (msg_in == msg_out) {
log() << "Full response read. Will take a short nap.";
if (EchoClientTask *t = dynamic_cast<EchoClientTask *>(owner()))
t->test_succeeded();
} else {
log() << "Unexpected response. Nevermind. Will try again soon.";
}
return PollState::KEEPALIVE;
} else if (msg_in.size() < msg_out.size()) {
log() << "More data needed";
return PollState::READ;
} else {
log() << "Got unexpected data: sent " << msg_out << ", got " << msg_in;
return PollState::CLOSE;
}
}

View file

@ -0,0 +1,17 @@
#pragma once
#include <framework/socketconnection.h>
class EchoClientTask;
class EchoClientConnection : public SocketConnection {
public:
EchoClientConnection(EchoClientTask *task, const std::string &hostname,
unsigned int port);
PollState connected() override;
PollState readData(char *buf, size_t len) override;
private:
size_t sent = 0;
std::string msg_in, msg_out;
};

View file

@ -0,0 +1,20 @@
#include "echoclienttask.h"
#include "echoclientconnection.h"
double EchoClientTask::start() {
log() << "Will create EchoClientConnection";
if (!addConnection(new EchoClientConnection(this, _hostname, _port)))
setError("Cannot find server");
return 1.0;
}
double EchoClientTask::timerEvent() {
if (elapsed() > 15.0) {
setResult("Timeout!");
return 0;
}
log() << "Create another EchoClientConnection";
if (!addConnection(new EchoClientConnection(this, _hostname, _port)))
setError("Cannot find server");
return 1.0;
}

View file

@ -0,0 +1,35 @@
#pragma once
#include <framework/task.h>
class EchoClientTask : public Task {
public:
EchoClientTask(const std::string &host, unsigned int port) :
Task("EchoClientTask"),
_port(port),
_hostname(host) {
}
void test_succeeded() {
++no_succeeded_requests;
if (no_succeeded_requests == 10)
setResult("All done!");
}
double start() override;
void connAdded(SocketConnection *) override {
log() << "connection added";
}
void connRemoved(SocketConnection *) override {
log() << "connection removed";
}
double timerEvent() override;
private:
uint16_t _port;
std::string _hostname;
unsigned int no_succeeded_requests = 0;
};

View file

@ -0,0 +1,9 @@
TARGET = cliclient
DIRLEVEL = ../..
LOGLEVEL = dbg
GNUTLS = 0
SOURCES = cliclient.cpp webserver.cpp
CLEAN += log.txt
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,98 @@
#include <fstream>
#include <framework/eventloop.h>
#include <framework/synchronousbridge.h>
#include "webserver.h"
/* Example 020: Demonstration of the agent/bridge/client concept.
This program contains a CLI (command line interface) to a WebServer task.
It must be run in a terminal. The CLI is rather pointless, it just writes
the latest URL fetched from the WebServer in a single line in the terminal.
However, the same technique can be used to implement real user interfaces,
e.g. a GUI or a web interface.
A log is written to log.txt.
The WebServer task is an _agent_, i.e. it can communicate with a client that
does not run in the eventloop. The client is the CLI which is implemented by
the MyClient class below.
The communication between the agent and the client is facilitated by a _bridge_
which is a task running in the eventloop. The bridge can pass messages from
the agent to the client and from the client to the agent. Each message is a
single string.
The bridge class is a subclass of the BridgeTask class, specifying the API from
the agent's point of view. Since the client is not a Task (it could be anything
really), different BridgeTask sublasses have different means of communicating
with the client. However, the agent code does not depend on what client is used.
In this example, the agent and the client run in the same process. The most
common case though is that the client runs in another process or at least not
in the same thread as the eventloop.
To test the CLI client, run it in a terminal. Then retrieve a few URLs from the
agent, which runs on port 8080, e.g.
http://127.0.0.1:8080/getTime
http://127.0.0.1:8080/getStats
http://127.0.0.1:8080/hi
The program will (as a demonstration) terminate after a few requests.
*/
class MyClient : public SynchronousClient {
public:
// initial messages to the agent shall be pushed onto return_msgs.
void initialMsgToAgent(std::deque<std::string> &return_msgs) override;
// msg is a new message from the agent.
// push any return messages onto return_msgs.
void newEventFromAgent(std::deque<std::string> &return_msgs,
const std::string &msg) override;
private:
std::string last_msg;
unsigned int msgCount = 0;
};
void MyClient::initialMsgToAgent(std::deque<std::string> &return_msgs) {
return_msgs.push_back("client ready");
}
void MyClient::newEventFromAgent(std::deque<std::string> &return_msgs,
const std::string &msg) {
++msgCount;
// Move back to leftmost position in the terminal
std::cerr << std::string(last_msg.size(), '\010') << msg;
if (last_msg.size() > msg.size()) {
// New message is shorter, erase last part of old message in terminal
size_t n = static_cast<size_t>(last_msg.size()-msg.size());
std::cerr << std::string(n, ' ') << std::string(n, '\010');
}
last_msg = msg;
// Just as a demonstration, we will send a termination request to the agent
// after having received a few messages.
if (msgCount == 4)
return_msgs.push_back("quit");
if (BridgeTask::isAgentTerminatedMessage(msg)) {
// Agent gone.
std::cerr << "\nBye." << std::endl;
}
}
int main(int , char *[]) {
std::ofstream log("log.txt");
Logger::setLogFile(log);
EventLoop eventloop("EventLoop");
WebServer *agent = new WebServer("listen 8080");
MyClient *client = new MyClient();
eventloop.addTask(new SynchronousBridge(agent, client));
eventloop.runUntilComplete();
return 0;
}

View file

@ -0,0 +1,57 @@
#include "webserver.h"
#include <framework/bridgetask.h>
// Messages from the client arrive here:
void WebServer::handleExecution(Task *sender, const std::string &message) {
dbg_log() << "Event " << message << " from " << sender->label();
if (BridgeTask *bridge = dynamic_cast<BridgeTask *>(sender)) {
if (the_bridge && bridge != the_bridge) {
// We already had a connected bridge and a new one tries to connect.
// We could allow several simultaneous clients, but normally one is
// enough.
log() << "Won't allow more than one client";
return;
}
// Here we can handle all kinds of messages from the client.
if (message == "client ready") {
// Client is ready to receive messages from us.
the_bridge=bridge;
} else if (message == "quit" ) {
setResult("Terminate by request from client");
}
}
}
void WebServer::taskFinished(Task *task) {
if (task == the_bridge) {
log() << "Client connection broken";
the_bridge = nullptr;
// We could call setResult("") to terminate
// but will wait for a new client instead.
}
}
HttpState WebServer::newGetRequest(HttpServerConnection *conn,
const std::string &uri) {
++tot_no_requests;
log() << "URI: " << uri << " #" << tot_no_requests;
if (the_bridge) {
std::string msg = "Last request: " + uri + " (request #" +
std::to_string(tot_no_requests) + ")";
the_bridge->sendMsgToClient(msg);
}
if (uri == "/getTime")
conn->sendHttpResponse(headers("200 OK"), "text/plain", dateString());
else if (uri == "/getStats")
conn->sendHttpResponse(headers("200 OK"), "text/plain",
std::to_string(tot_no_requests));
else if (uri == "/hi")
conn->sendHttpResponse(headers("200 OK"), "text/plain", "hello");
else
conn->sendHttpResponse(headers("404 Not Found"), "text/plain",
"unknown service");
return HttpState::WAITING_FOR_REQUEST;
}

View file

@ -0,0 +1,23 @@
#pragma once
#include <http/webservertask.h>
class BridgeTask;
class WebServer : public WebServerTask {
public:
WebServer(const std::string &cfg) :
WebServerTask("WebServer", cfg) {
}
HttpState newGetRequest(HttpServerConnection *,
const std::string &uri) override;
void handleExecution(Task *sender, const std::string &message) override;
void taskFinished(Task *task) override;
private:
unsigned long tot_no_requests = 0;
// We will only talk to one client.
BridgeTask *the_bridge = nullptr;
};

View file

@ -0,0 +1,11 @@
TARGET = client
DIRLEVEL = ../..
LOGLEVEL = dbg
GNUTLS = 0
THREADS = 0
SOURCES = main.cpp
CXXFLAGS += -g
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,76 @@
#include <http/httprequestengine.h>
#include <json11/json11.hpp>
#include <framework/eventloop.h>
class MainTask : public Task {
public:
MainTask() : Task("Main") {
bot = new HttpRequestEngine("MyBot", HttpHost("127.0.0.1", 8080),
0, 3);
//bot = new HttpRequestEngine("MyBot", "lab04.bredbandskollen.se");
}
~MainTask() {
}
double start() override {
dbg_log() << "starting";
addNewTask(bot, this);
bot->startObserving(this);
bot->getJob(this, "Ticks " + std::to_string(++request_no),
"/chunk?ticks=10");
bot->getJob(this, "Count " + std::to_string(++request_no),
"/getStats");
killChildTaskWhenFinished();
return 3.0;
}
double timerEvent() override {
dbg_log() << "timerEvent";
if (request_no < 10)
bot->getJob(this, "Ticks " + std::to_string(++request_no),
"/chunk?ticks=10");
bot->getJob(this, "Count " + std::to_string(++request_no),
"/getStats");
if (request_no < 20)
return 3.0;
else
return 0.0;
}
void taskFinished(Task *task) override {
log() << "Oops, task " << task->label() << " died.";
if (task == bot) {
bot = nullptr;
setResult("Failed");
}
}
void handleExecution(Task *task, const std::string &name) override {
log() << task->label() << " event: " << name;
if (task != bot)
return;
if (bot->httpStatus()) {
log() << "Exit job " << name << " result: " << bot->contents();
++ok_jobs;
if (ok_jobs == 20)
setResult("All done.");
} else {
log() << "Exit job " << name << " failed, will retry";
bot->redoJob();
}
}
private:
unsigned int request_no = 0, ok_jobs = 0;
HttpRequestEngine *bot = nullptr;
std::string _ticket;
};
int main(int , char *[]) {
EventLoop eventloop("EventLoop");
eventloop.addTask(new MainTask());
eventloop.runUntilComplete();
return 0;
}

15
src/examples/Makefile Normal file
View file

@ -0,0 +1,15 @@
# These can't be built in parallel since there's a race when creating
# the dependency files, so "make" / "make -j1" is needed.
SUBDIRS := $(wildcard [0-9][0-9][0-9]_*)
all: $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@ clean # needed because of conflicting obj instrumentations
$(MAKE) -C $@ -j # internal parallel build is ok
clean:
@$(foreach dir,$(SUBDIRS),$(MAKE) -C$(dir) clean;)
.PHONY: $(SUBDIRS) clean all

View file

@ -0,0 +1,20 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include "../json11/json11.hpp"
#include "bridgetask.h"
BridgeTask::~BridgeTask() {
}
double BridgeTask::start() {
if (the_agent) {
addNewTask(the_agent, this);
the_agent->startObserving(this);
} else {
err_log() << "You must call setAgent before starting bridge";
setError("no agent");
}
return 0;
}

142
src/framework/bridgetask.h Normal file
View file

@ -0,0 +1,142 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <string>
#include "task.h"
/// \brief
/// Tasks may use a bridge to communicate with an application running
/// outside the event loop.
///
/// This class provides an abstract interface for a _bridge_ which facilitates
/// communication between a task and code running outside the event loop.
/// The code might run in another process or thread. The actual
/// means of communication might be sockets or pipes, or shared variables in
/// the case of threads, or something else entirely.
///
/// The task running in the event loop is called the _agent_, and the outside
/// aplication is called the _client_.
///
/// To use this class, you must
/// - derive from this class
/// - implement the sendMessageToClient method
/// - implement a way for the client to send messages back, and pass those
/// messages through the sendMsgToAgent method
/// - if you override the start() method, call BridgeTask::start() first
/// - create an agent object and a bridge object
/// - add the bridge object to the event loop
/// The agent task will be aborted when the bridge task is finished.
/// If you need timers (e.g. for polling), you must override the start() method
/// below and your start() method must call BridgeTask::start().
///
/// When there is a message from the client to the agent, the agent's
/// Task::handleExecution method will be called with two parameters:
/// the bridge object and a message.
///
/// Typically, the agent's Task::handleExecution method checks if the
/// first parameter can be cast to a BridgeTask pointer.
/// If so, the second parameter is a message from the client.
/// When the first message from the client arrives, the agent stores
/// the pointer to the bridge in case it needs to send messages back.
class BridgeTask : public Task {
public:
/// \brief
/// Create a bridge to the given agent task.
///
/// If the agent task isn't available yet, it must be added using
/// BridgeTask::setAgent before the bridge is added to the EventLoop.
BridgeTask(Task *agent = nullptr) : Task("Bridge"), the_agent(agent) {
killChildTaskWhenFinished();
}
/// \brief
/// Will add the agent task to the EventLoop.
///
/// *Note!* If you override this method, it should be called explicitly by
/// the overriding method!
double start() override;
/// If the agent dies, a special message will be sent to notify the client.
void taskFinished(Task *task) override {
if (task == the_agent) {
if (!task->result().empty()) {
log() << "Agent terminated";
sendMsgToClient(agentTerminatedMessage(task->result()));
the_agent = nullptr;
}
die();
}
}
/// If the agent task was created after the bridge, it must be added using
/// this method _before_ the bridge is added to the EventLoop.
void setAgent(Task *agent) {
if (!the_agent && !hasStarted())
the_agent = agent;
else
err_log() << "cannot set agent";
}
/// Terminate the bridge task.
void die() {
setResult("");
}
/// The agent will call this to pass messages to the client.
virtual void sendMsgToClient(const std::string &msg) = 0;
/// \brief Format a message to the agent.
///
/// Recommended way to format messages to the agent: a method name,
/// and "arguments" stored in a JSON object.
///
/// *Note!* The method name must not contain any special chararcters.
static std::string msgToAgent(const std::string &method,
const std::string &jsonobj = "{}") {
return "{\"method\": \"" + method + "\", \"args\": " + jsonobj + "}";
}
/// Return true if msg is formatted as a note that the agent has terminated
/// and that there will be no more messages.
static bool isAgentTerminatedMessage(const std::string &msg) {
return (msg.substr(0, 12) == "AGENT EXIT: ");
}
/// Format a message to signal that the Agent is gone.
static std::string agentTerminatedMessage(const std::string &err_msg) {
return "AGENT EXIT: " + err_msg;
}
/// \brief Format a message to the client.
///
/// Recommended way to format messages to the client: an event (or method)
/// name, and "arguments" stored in a JSON object.
///
/// *Note!* The method name must not contain any special chararcters.
void sendMsgToClient(const std::string &method, const std::string &jsonobj) {
sendMsgToClient("{\"event\": \"" + method +
"\", \"args\": " + jsonobj + "}");
}
virtual ~BridgeTask() override;
protected:
/// Send message to the agent.
void sendMsgToAgent(const std::string &msg) {
executeHandler(the_agent, msg);
if (msg.substr(0, terminate_msg.size()) == terminate_msg)
die();
}
/// Format and send message to the agent.
void sendMsgToAgent(const std::string &method, const std::string &jsonobj) {
sendMsgToAgent(msgToAgent(method, jsonobj));
}
private:
Task *the_agent;
//const std::string quit_msg = "quit";
const std::string terminate_msg = "{\"method\": \"terminate\"";
};

644
src/framework/engine.cpp Normal file
View file

@ -0,0 +1,644 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <string.h>
#ifdef _WIN32
#include <winsock2.h>
#else
#include <sys/socket.h>
#include <netdb.h>
#endif
#ifdef _WIN32
#include <ws2tcpip.h>
#else
#include <sys/select.h>
#endif
#include <set>
#include <stdexcept>
#include "engine.h"
#include "socketconnection.h"
#include "serversocket.h"
#ifndef _WIN32
#include <arpa/inet.h>
#endif
Engine::Engine(std::string label) :
Logger(label) {
#ifdef USE_GNUTLS
x509_cred.resize(1);
if (gnutls_global_init() < 0 ||
gnutls_certificate_allocate_credentials(&x509_cred[0]) < 0 ||
gnutls_priority_init(&priority_cache,
"PERFORMANCE:%SERVER_PRECEDENCE",
nullptr) < 0)
throw std::runtime_error("cannot enable TLS");
#if GNUTLS_VERSION_NUMBER >= 0x030506
/* only available since GnuTLS 3.5.6, on previous versions see
* gnutls_certificate_set_dh_params(). */
gnutls_certificate_set_known_dh_params(x509_cred[0], GNUTLS_SEC_PARAM_MEDIUM);
#endif
#endif
}
Engine::~Engine() {
terminate(0);
Socket::clearCache();
#ifdef USE_GNUTLS
for (auto &cred : x509_cred)
gnutls_certificate_free_credentials(cred);
gnutls_priority_deinit(priority_cache);
gnutls_global_deinit();
#endif
}
#ifdef USE_GNUTLS
bool Engine::setCABundle(const std::string &path) {
if (path.empty())
return true;
ca_bundle = path;
if (gnutls_certificate_set_x509_trust_file(x509_cred[0], ca_bundle.c_str(),
GNUTLS_X509_FMT_PEM) < 0)
return false;
return true;
}
bool Engine::tlsSetKey(ServerSocket *conn, const std::string &crt_path,
const std::string &key_path,
const std::string &password) {
if (key_path.empty() && crt_path.empty())
return 0;
unsigned int index;
auto it = tls_crt_map.find(crt_path);
if (it != tls_crt_map.end()) {
index = it->second;
} else {
index = static_cast<unsigned int>(x509_cred.size());
x509_cred.resize(index+1);
if (gnutls_certificate_allocate_credentials(&x509_cred[index]) < 0) {
x509_cred.resize(index);
return false;
}
int ret = gnutls_certificate_set_x509_key_file2(x509_cred[index],
crt_path.c_str(),
key_path.c_str(),
GNUTLS_X509_FMT_PEM,
password.c_str(), 0);
if (ret < 0) {
err_log() << "Cannot add TLS server certificate: "
<< gnutls_strerror(ret);
return false;
}
tls_crt_map[crt_path] = index;
}
conn->tlsSetKey(index);
return true;
}
#endif
// Hand the SocketConnection object over to us.
// Will delete the object and return false on immediate failure.
// If the object already has a socket, i.e. conn->socket()>=0,
// it will be used.
// Otherwise we will listen on the given port number and
// local ip number (port 0 means any available port,
// leave ip_number empty to listen on all local ip numbers).
bool Engine::addServer(ServerSocket *conn) {
if (!conn)
return false;
if (conn->socket()<0 && !conn->createServerSocket()) {
err_log() << "cannot add ServerSocket "
<< conn->hostname() << ':' << conn->port();
delete conn;
return false;
}
dbg_log() << "addServer fd " << conn->socket();
connectionStore[conn->socket()] = conn;
return true;
}
// Hand the SocketConnection object over to us.
// Will delete the object and return false on immediate failure.
// Otherwise return true; then either connected or connectionFailed
// will be called on conn.
bool Engine::addClient(SocketConnection *conn) {
if (!conn)
return false;
#ifndef _WIN32
if (conn->socket() >= 0) {
if (connectionStore.find(conn->socket()) != connectionStore.end()) {
err_log() << "Socket " << conn->socket() << " host "
<< conn->hostname() << " already exists";
return false;
}
if (conn->hostname() != "UnixDomain" || !conn->socket()) {
err_log() << "Socket " << conn->socket() << " host "
<< conn->hostname() << " not accepted";
return false;
}
// Unix Domain socket, socket already created and connected
addConnected(conn);
return true;
}
#endif
std::string label = conn->cacheLabel();
auto p = keepaliveCache.find(label);
if (!label.empty() &&
p != keepaliveCache.end()) {
log() << "Using cached socket " << p->second << " label " << label;
conn->setSocket(p->second);
#ifdef USE_GNUTLS
auto it = tls_session_cache.find(p->second);
if (it != tls_session_cache.end()) {
conn->insert_cached_session(it->second);
tls_session_cache.erase(it);
}
#endif
keepaliveCache.erase(p);
//log() << "Cache size is " << keepaliveCache.size();
conn->setState(conn->connected());
} else if (!conn->asyncConnect()) {
delete conn;
return false;
}
connectionStore[conn->socket()] = conn;
return true;
}
void Engine::childProcessCloseSockets() {
for (auto p : connectionStore)
Socket::closeSocket(p.first);
for (auto p : keepaliveCache)
Socket::closeSocket(p.second);
}
void Engine::terminate(unsigned int ) {
while (!connectionStore.empty())
killConnection(connectionStore.cbegin()->first);
for (auto p : keepaliveCache)
Socket::closeSocket(p.second);
#ifdef USE_GNUTLS
for (auto p : tls_session_cache)
gnutls_deinit(p.second);
#endif
}
bool Engine::reclaimConnections() {
std::set<int> fds_to_remove;
for (auto p : connectionStore)
switch (p.second->state()) {
case PollState::CLOSE:
case PollState::KILL:
case PollState::KEEPALIVE:
fds_to_remove.insert(p.first);
break;
case PollState::NONE:
case PollState::READ_BLOCKED:
#ifdef USE_GNUTLS
case PollState::TLS_HANDSHAKE:
#endif
case PollState::CONNECTING:
case PollState::WRITE:
case PollState::READ_WRITE:
case PollState::READ:
break;
}
if (fds_to_remove.empty())
return false;
for (auto fd : fds_to_remove)
killConnection(fd);
return true;
}
int Engine::setFds(fd_set &r, fd_set &w, fd_set &e) {
int max = -1;
FD_ZERO(&r);
FD_ZERO(&w);
FD_ZERO(&e);
// Check what to do with each connection.
// Note! We cannot do anything non-trivial while looping
// over connectionStore since elements might be added or
// removed, invalidating iterators.
for (auto p : connectionStore) {
int fd = p.first;
Socket *c = p.second;
switch (c->state()) {
case PollState::CLOSE:
case PollState::KILL:
case PollState::KEEPALIVE:
continue;
case PollState::CONNECTING:
case PollState::WRITE:
case PollState::READ_WRITE:
FD_SET(fd, &w);
break;
case PollState::READ:
if (c->wantToSend())
FD_SET(fd, &w);
break;
#ifdef USE_GNUTLS
case PollState::TLS_HANDSHAKE:
#endif
case PollState::NONE:
break;
case PollState::READ_BLOCKED:
c->setState(c->checkReadBlock());
if (c->wantToSend())
FD_SET(fd, &w);
FD_SET(fd, &e);
if (fd > max)
max = fd;
continue;
}
// Always check for readability (if closed by peer) and error:
FD_SET(fd, &r);
FD_SET(fd, &e);
if (fd > max)
max = fd;
}
// Check keep-alive connection since they may be closed by peer.
for (auto p : keepaliveCache) {
FD_SET(p.second, &r);
FD_SET(p.second, &e);
if (p.second > max)
max = p.second;
}
return max;
}
#ifdef USE_THREADS
thread_local
#endif
volatile bool Engine::yield_called = false;
bool Engine::max_open_fd_reached = false;
void Engine::handleMaxOpenFdReached() {
// TODO thread safe
if (!keepaliveCache.empty()) {
auto it = keepaliveCache.begin();
Socket::closeSocket(it->second);
#ifdef USE_GNUTLS
auto p = tls_session_cache.find(it->second);
if (p != tls_session_cache.end())
tls_session_cache.erase(p);
#endif
keepaliveCache.erase(it);
max_open_fd_reached = false;
return;
}
if (connectionStore.size() > 100) {
// Try to find a client socket to remove
for (auto &p : connectionStore)
if (auto s = dynamic_cast<SocketConnection *>(p.second)) {
warn_log() << "Out of file descriptors, will remove "
<< s->id();
s->closeMe();
max_open_fd_reached = false;
return;
}
}
// Close some random file descriptor
warn_log() << "Out of file descriptors, possible fd leak";
static int fd_to_remove = 10;
while (++fd_to_remove < 65536) {
if (connectionStore.find(fd_to_remove) == connectionStore.end()) {
if (!Socket::closeSocket(fd_to_remove)) {
warn_log() << "Possible fd leak, closed fd " << fd_to_remove;
max_open_fd_reached = false;
return;
}
}
}
warn_log() << "Out of file descriptors, cannot recover";
fd_to_remove = 10;
}
// Handle network events for max_time seconds or until yield is called.
bool Engine::run(double max_time) {
deadline = timeAfter(max_time);
yield_called = false;
{
// Find sockets that will expire and delete them.
// Can't delete them while looping over connectionStore since
// connectionStore gets modified when Socket objects are deleted.
std::vector<int> expired;
for (auto p : connectionStore)
if (p.second->hasExpired(deadline))
expired.push_back(p.first);
for (auto fd : expired)
killConnection(fd);
}
while (!yield_called) {
if (reclaimConnections())
continue; // Recheck after callbacks
fd_set readFds, writeFds, errFds;
int max_fd = setFds(readFds, writeFds, errFds);
struct timeval timeout;
{
double time_left = secondsTo(deadline);
long us = static_cast<long>(1e6*time_left);
if (us <= 0)
return true;
timeout.tv_sec = us/1000000;
timeout.tv_usec = us%1000000;
}
int res = select(max_fd + 1, &readFds, &writeFds, &errFds, &timeout);
if (res < 0) {
if (fatalSelectError())
return false;
} else {
doFds(readFds, writeFds, errFds, max_fd);
if (max_open_fd_reached)
handleMaxOpenFdReached();
}
}
reclaimConnections();
return true;
}
void Engine::doFds(const fd_set &r, const fd_set &w, const fd_set &e, int max) {
for (auto it=keepaliveCache.begin(); it != keepaliveCache.end(); ) {
int fd = it->second;
if (FD_ISSET(fd, &r) || FD_ISSET(fd, &e)) {
log() << "close keepalive socket " << fd;
#ifdef USE_GNUTLS
auto p = tls_session_cache.find(fd);
if (p != tls_session_cache.end()) {
gnutls_deinit(p->second);
tls_session_cache.erase(p);
}
#endif
Socket::closeSocket(fd);
it = keepaliveCache.erase(it);
} else
++it;
}
for (int fd=0; fd<=max; ++fd) {
// We cannot loop over connectionStore since it may
// be modified in all sorts of ways within the loop.
auto p = connectionStore.find(fd);
if (p == connectionStore.end())
continue;
if (FD_ISSET(fd, &e)) {
err_log() << "socket error " << fd;
killConnection(fd);
continue;
}
Socket *conn = p->second;
bool readable = FD_ISSET(fd, &r);
bool writable = FD_ISSET(fd, &w);
if (!readable && !writable)
continue;
SocketConnection *c = dynamic_cast<SocketConnection *>(conn);
if (!c) {
// Is it a server connection?
if (ServerSocket *srv =
dynamic_cast<ServerSocket *>(conn)) {
// New connection on listen socket
handleIncoming(srv);
continue;
}
err_log() << "event on non-client connection";
killConnection(fd);
continue;
}
if (readable && (c->state() == PollState::READ ||
c->state() == PollState::READ_WRITE)) {
// React to reading before considering writing:
c->setState(c->doRead(fd));
// If it switches to sending, we'll probably be able to send some data
// immediately, saving us a select roundtrip.
if (c->state() == PollState::WRITE) {
writable = true;
} else {
continue;
}
}
if (writable) {
PollState s;
if (c->state() == PollState::CONNECTING) {
if (c->inError()) {
err_log() << "async connect failed";
killConnection(fd);
continue;
}
#ifdef USE_GNUTLS
if (c->use_tls) {
if (!c->init_tls_client(x509_cred[0], !ca_bundle.empty())) {
log() << "cannot enable TLS" << fd;
killConnection(fd);
continue;
}
int ret = c->try_tls_handshake();
if (ret >= 0) {
s = c->connected();
} else if (gnutls_error_is_fatal(ret)) {
err_log() << "TLS handshake failure: " << gnutls_strerror(ret);
killConnection(fd);
continue;
} else {
s = PollState::TLS_HANDSHAKE;
}
} else
#endif
s = c->connected();
} else {
s = c->doWrite();
}
c->setState(s);
continue;
}
#ifdef USE_GNUTLS
if (c->state() == PollState::TLS_HANDSHAKE) {
int ret = c->try_tls_handshake();
if (ret >= 0) {
c->setState(c->connected());
} else if (gnutls_error_is_fatal(ret)) {
err_log() << "TLS handshake failure: " << gnutls_strerror(ret);
killConnection(fd);
}
continue;
}
#endif
// The socket is readable, but we don't want to read it
// since state is neither READ nor READ_WRITE.
// The connection may have been closed by peer.
PollState s = c->doRead(fd);
c->setState(s);
}
}
bool Engine::fatalSelectError() {
if (Socket::isTempError()) {
errno_log() << "select interrupt";
return false;
}
// Oops, we've done something really wrong: an invalid
// filedescriptor may have been added to the poll set.
// Find it and remove it, then try again.
errno_log() << "select error";
for (auto p : connectionStore)
if (p.second->inError()) {
p.second->setState(PollState::CLOSE);
killConnection(p.first);
return false;
}
for (auto p=keepaliveCache.begin(); p != keepaliveCache.end(); ++p)
if (Socket::socketInError(p->second)) {
#ifdef USE_GNUTLS
auto it = tls_session_cache.find(p->second);
if (it != tls_session_cache.end()) {
gnutls_deinit(it->second);
tls_session_cache.erase(it);
}
#endif
Socket::closeSocket(p->second);
keepaliveCache.erase(p);
return false;
}
// Don't know what's wrong, cannot recover.
return true;
}
void Engine::killConnection(int fd) {
auto p = connectionStore.find(fd);
if (p != connectionStore.end()) {
Socket *conn = p->second;
SocketConnection *c = dynamic_cast<SocketConnection *>(conn);
if (conn->state() == PollState::KEEPALIVE) {
std::string label = conn->cacheLabel();
if (!label.empty()) {
log() << "keep-alive socket " << fd << " to " << label;
keepaliveCache.insert(std::make_pair(label, fd));
#ifdef USE_GNUTLS
if (c && c->is_tls())
tls_session_cache[fd] = c->cache_session();
#endif
// Otherwise the socket will get closed when conn is deleted:
conn->setSocket(-1);
}
}
if (Task *task = conn->owner()) {
if (c)
task->connRemoved(c);
else if (ServerSocket *srv = dynamic_cast<ServerSocket *>(conn))
task->serverRemoved(srv);
}
delete conn;
connectionStore.erase(p);
}
}
std::set<Socket *> Engine::findSockByTask(const Task *t) const {
std::set<Socket *> cset;
for (auto p : connectionStore)
if (p.second->owner() == t)
cset.insert(p.second);
return cset;
}
void Engine::wakeUpByTask(Task *t) {
for (auto p : connectionStore)
if (SocketConnection *c = dynamic_cast<SocketConnection *>(p.second))
if (c->owner() == t && c->state() == PollState::NONE)
c->setState(c->connected());
}
bool Engine::wakeUpConnection(SocketConnection *s) {
if (s->state() != PollState::NONE)
return false;
s->setState(s->connected());
return true;
}
void Engine::deleteConnByTask(const Task *task) {
std::set<int> toRemove;
for (auto p : connectionStore)
if (p.second->owner() == task)
toRemove.insert(p.first);
for (auto fd : toRemove)
killConnection(fd);
}
void Engine::cancelConnection(SocketConnection *s) {
killConnection(s->socket());
}
void Engine::handleIncoming(ServerSocket *server) {
SocketConnection *client = server->incoming();
if (!client) {
return;
}
int newfd = client->socket();
#ifdef USE_GNUTLS
log() << "Socket " << newfd << " TLS=" << server->tlsKey();
if (server->tlsKey()) {
if (!client->init_tls_server(x509_cred[server->tlsKey()], priority_cache)) {
log() << "cannot enable TLS" << newfd;
delete client;
return;
}
client->enableTLS();
int ret = client->try_tls_handshake();
if (ret >= 0) {
client->setState(client->connected());
} else if (gnutls_error_is_fatal(ret)) {
err_log() << "TLS handshake has failed: " << gnutls_strerror(ret);
delete client;
return;
} else {
client->setState(PollState::TLS_HANDSHAKE);
}
} else
#endif
client->setState(client->connected());
connectionStore[newfd] = client;
server->owner()->connAdded(client);
}
void Engine::addConnected(SocketConnection *conn) {
dbg_log() << "addConnected " << conn->socket();
connectionStore[conn->socket()] = conn;
conn->owner()->connAdded(conn);
conn->setState(conn->connected());
}

151
src/framework/engine.h Normal file
View file

@ -0,0 +1,151 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
// This class implements a network engine, managing sockets.
// It is only used by the EventLoop class.
#pragma once
#include <thread>
#include <map>
#include <vector>
#include <string>
#include <sys/types.h>
#include "logger.h"
#ifdef _WIN32
#include <winsock2.h>
#endif
class Socket;
class SocketConnection;
class ServerSocket;
class Task;
#ifdef USE_GNUTLS
#include <gnutls/gnutls.h>
#endif
/// \brief
/// The network engine.
///
/// Cannot be used directly. Will be managed by the EventLoop class.
class Engine : public Logger {
public:
bool addClient(SocketConnection *conn);
void addConnected(SocketConnection *conn);
bool addServer(ServerSocket *conn);
Engine(std::string label);
/// Will kill all remaining connections.
~Engine();
/// Close all connections, giving them at most
/// max_time_ms milliseconds to finish what's
/// already written to them.
void terminate(unsigned int max_time_ms);
/// Call this in child process to close all redundant sockets after fork.
void childProcessCloseSockets();
/// \brief
/// Run the "event loop" for at most `max_time` seconds.
///
/// Will return when all connections have been closed,
/// when a fatal error has occurred, or when max_time has passed.
/// Return value is false on fatal error, otherwise true.
/// You should call this function repetedly until you're done.
///
/// Don't set max_time > 2000 on 32-bit platforms.
bool run(double max_time);
/// Remove all connections owned by the task.
void deleteConnByTask(const Task *task);
/// Call this to make the Engine::run method return prematurely.
static void yield() {
yield_called = true;
}
/// \brief
/// Call this to enter a recovery mode if no more file descriptors
/// could be created.
static void notifyOutOfFds() {
// TODO: thread safe
max_open_fd_reached = true;
}
std::set<Socket *> findSockByTask(const Task *t) const;
/// Wake up all idle connections belonging to t:
void wakeUpByTask(Task *t);
/// Wake up connection s if it is idle, return false otherwise.
bool wakeUpConnection(SocketConnection *s);
void cancelConnection(SocketConnection *s);
/// Return true if connection still exists.
/// *Note!* We cannot _use_ conn if it has been deleted!
bool connActive(const Socket *conn) const {
for (auto it : connectionStore)
if (it.second == conn)
return true;
return false;
}
/// Call this to make the Engine::run method return earlier.
void resetDeadline(const TimePoint &t) {
if (deadline > t)
deadline = t;
}
#ifdef USE_GNUTLS
/// Set path to file containing chain of trust for SSL certificate.
bool setCABundle(const std::string &path);
/// Use SSL certificate for a listening socket.
bool tlsSetKey(ServerSocket *conn, const std::string &crt_path,
const std::string &key_path, const std::string &password);
#endif
private:
TimePoint deadline;
#ifdef USE_THREADS
thread_local
#endif
static volatile bool yield_called;
static bool max_open_fd_reached;
void handleMaxOpenFdReached();
void killConnection(int fd);
void handleIncoming(ServerSocket *server);
// Check all connections, remove closed, reclaim keepalive,
// return false if none was removed:
bool reclaimConnections();
// Prepare for select, return largest fd or -1:
int setFds(fd_set &r, fd_set &w, fd_set &e);
// Check error from select, return false if fatal:
bool fatalSelectError();
// Take care of all network events:
void doFds(const fd_set &r, const fd_set &w, const fd_set &e, int max);
// Map socket number to Socket object:
std::map<int, Socket *> connectionStore;
// Cache of open connections that may be reused.
// Value is socket number. Key is some kind of label,
// chosen by the tasks owning the connections,
// that should identify the type of connection.
std::multimap<std::string, int> keepaliveCache;
#ifdef USE_GNUTLS
std::map<int, gnutls_session_t> tls_session_cache;
std::string ca_bundle;
std::map<std::string, unsigned int> tls_crt_map;
// Index 0 is used for outgoing connection, containing trust store.
// At index > 0 certificates for server sockets are stored.
std::vector<gnutls_certificate_credentials_t> x509_cred;
gnutls_priority_t priority_cache;
#endif
};

726
src/framework/eventloop.cpp Normal file
View file

@ -0,0 +1,726 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <algorithm>
#include <stdexcept>
#include <csignal>
#include "eventloop.h"
#include "engine.h"
#include "task.h"
#include "socketconnection.h"
#include "serversocket.h"
#ifdef USE_THREADS
//thread_local
#endif
volatile int EventLoop::got_signal = 0;
#ifdef _WIN32
void EventLoop::signalHandler(int signum) {
EventLoop::interrupt();
got_signal = signum;
}
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include "socketreceiver.h"
#include "workerprocess.h"
#ifdef USE_THREADS
thread_local
#endif
std::map<int, int> EventLoop::terminatedPIDs;
#ifdef USE_THREADS
thread_local
#endif
volatile int EventLoop::terminatedPIDtmp[100] = { 0 };
#ifdef USE_THREADS
thread_local
#endif
std::string EventLoop::openFileOnSIGHUP;
void EventLoop::signalHandler(int signum) {
EventLoop::interrupt();
if (signum != SIGCHLD) {
got_signal = signum;
return;
}
while (true) {
int wstatus;
pid_t pid = waitpid(-1, &wstatus, WNOHANG);
if (pid <= 0)
break;
//Logger::log("signalHandler") << "Child PID " << pid
// << " terminated, status " << wstatus;
for (size_t i=0; i<sizeof(terminatedPIDtmp)/sizeof(int); i+=2)
if (!terminatedPIDtmp[i]) {
terminatedPIDtmp[i] = pid;
terminatedPIDtmp[++i] = wstatus;
return;
}
// Bad, shouldn't use terminatedPIDs asynchronously. Won't happen if
// terminatedPIDtmp is large enough, i.e. twice number of chldren.
EventLoop::terminatedPIDs[pid] = wstatus;
}
}
int EventLoop::externalCommand(Task *owner, const char *const argv[]) {
if (!argv[0])
return false;
pid_t chld = fork();
if (chld < 0) {
errno_log() << "cannot fork";
return -1;
}
if (!chld) {
// TODO: Close all file descriptors in child!
int ret = execvp( argv[0], const_cast<char **>(argv) );
exit(ret);
}
pidOwner[chld] = owner;
return chld;
}
void EventLoop::daemonize() {
if (fork() != 0)
exit(0); // Close original process
setsid(); // Detach from shell
if (int fd = open("/dev/null", O_RDWR, 0) != -1) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO)
close(fd);
}
}
WorkerProcess *EventLoop::createWorker(Task *parent,
const std::string &log_file_name,
unsigned int channels,
unsigned int wno) {
auto old = openFileOnSIGHUP;
setLogFilename(log_file_name);
auto logfile = new std::ofstream(log_file_name, std::ios::app);
auto ret = createWorker(parent, logfile, channels, wno);
openFileOnSIGHUP = old;
return ret;
}
WorkerProcess *EventLoop::createWorker(Task *parent, std::ostream *log_file,
unsigned int channels,
unsigned int wno) {
if (aborted())
return nullptr;
std::vector<int> array(2 * channels);
int *pair_sd = array.data();
for (unsigned int i = 0; i<channels; ++i)
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pair_sd+2*i) < 0) {
if (errno == EMFILE)
Engine::notifyOutOfFds();
errno_log() << "cannot create socketpair";
return nullptr;
}
// TODO: second parameter: unsigned int noSocketReceivers, default 1
// different class for communication channels
int ppid = getpid();
pid_t wpid = fork();
if (wpid < 0)
return nullptr;
if (wpid > 0) {
// In parent (original) process
log() << "started worker " << wpid;
if (log_file)
delete log_file;
std::vector<SocketReceiver *> receivers;
for (unsigned int i = 0; i<2*channels; i+=2) {
close(pair_sd[i+1]);
fcntl(pair_sd[i], F_SETFL, O_NONBLOCK);
SocketReceiver *conn = new SocketReceiver(parent, pair_sd[i], wpid);
if (!addServer(conn)) {
err_log() << "Cannot create worker channel";
while (i < 2*channels)
close(pair_sd[i++]);
return nullptr;
}
log() << "Worker channel fd " << pair_sd[i];
receivers.push_back(conn);
}
pidOwner[wpid] = parent;
return new WorkerProcess(wpid, receivers);
}
// In worker (new child process)
engine.childProcessCloseSockets();
Task::supervisor = nullptr;
// Set new log file for child process
if (!log_file) {
log_file = new std::ostringstream();
log_file->clear(std::istream::eofbit);
}
setLogFile(*log_file);
EventLoop eventloop("Worker");
Task *worker = parent->createWorkerTask(wno);
if (!worker) {
err_log() << "Cannot create worker " << wno << ", will exit";
exit(1);
}
eventloop.addTask(worker);
for (unsigned int i = 0; i<2*channels; i+=2) {
close(pair_sd[i]);
fcntl(pair_sd[i+1], F_SETFL, O_NONBLOCK);
SocketReceiver *chan = new SocketReceiver(worker, pair_sd[i+1], ppid);
worker->newWorkerChannel(chan, i/2);
worker->addServer(chan);
}
eventloop.runUntilComplete();
parent->finishWorkerTask(wno);
log() << "Exit" << std::endl;
delete log_file;
exit(0);
}
void EventLoop::killChildProcesses(int signum) {
for (auto p : pidOwner)
kill(p.first, signum);
}
#endif
EventLoop::~EventLoop() {
removeAllTasks();
Task::supervisor = nullptr;
log() << "EventLoop finished.\n";
}
#ifdef USE_THREADS
void EventLoop::do_init(EventLoop *parent) {
parent_loop = parent;
#else
void EventLoop::do_init() {
#endif
if (Task::supervisor) {
throw std::runtime_error("eventloop already exists");
}
Task::supervisor = this;
got_signal = 0;
#ifdef USE_THREADS
if (parent_loop)
return;
#endif
#ifdef _WIN32
// Windows specific sockets initialization
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
throw std::runtime_error("cannot initialize network");
signal(SIGTERM, signalHandler);
signal(SIGABRT, signalHandler);
#else
terminatedPIDs.clear();
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = signalHandler;
sigfillset(&sa.sa_mask);
if (sigaction(SIGCHLD, &sa, nullptr) ||
sigaction(SIGTERM, &sa, nullptr) ||
sigaction(SIGINT, &sa, nullptr) ||
sigaction(SIGHUP, &sa, nullptr) ||
sigaction(SIGQUIT, &sa, nullptr) ||
sigaction(SIGABRT, &sa, nullptr) ||
sigaction(SIGPIPE, &sa, nullptr))
errno_log() << "cannot add signal handler";
#endif
}
void EventLoop::addSignalHandler(int signum, void (*handler)(int, EventLoop &)) {
#ifdef _WIN32
if (signal(signum, signalHandler) == SIG_ERR) {
errno_log() << "cannot add handler for signal " << signum;
return;
}
#else
if (signum == SIGCHLD) {
err_log() << "must not set custom handler for SIGCHLD";
return;
}
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = signalHandler;
if (sigaction(signum, &sa, nullptr)) {
errno_log() << "cannot add handler for signal " << signum ;
return;
}
#endif
userSignalHandler.insert(std::make_pair(signum, handler));
}
void EventLoop::addTask(Task *task, Task *parent) {
if (task && tasks.find(task) == tasks.end()) {
tasks[task] = parent;
if (parent)
startObserving(parent, task);
if (task->terminated()) {
// Task finished when executing its constructor
notifyTaskFinished(task);
return;
}
double ts = task->begin();
log() << "Task " << task->label() << " timeout " << ts;
if (ts > 0) {
TimePoint t = timeAfter(ts);
timer_queue.insert(std::make_pair(t, task));
// Make sure we get back here when the timer expires:
engine.resetDeadline(t);
}
} else {
err_log() << "cannot add task";
}
}
bool EventLoop::addConnection(SocketConnection *conn) {
if (!conn)
return false;
if (tasks.find(conn->owner()) != tasks.end() &&
engine.addClient(conn)) {
conn->owner()->connAdded(conn);
return true;
}
err_log() << "cannot add connection";
return false;
}
bool EventLoop::addConnected(SocketConnection *conn) {
if (!conn)
return false;
if (tasks.find(conn->owner()) != tasks.end()) {
engine.addConnected(conn);
return true;
}
err_log() << "cannot add connection";
delete conn;
return false;
}
bool EventLoop::addServer(ServerSocket *conn) {
if (!conn)
return false;
if (tasks.find(conn->owner()) != tasks.end() &&
engine.addServer(conn)) {
conn->owner()->serverAdded(conn);
return true;
}
err_log() << "cannot add server connection";
return false;
}
// This is just a safeguard against buggy clients.
// Will be called _after_ object pointed to by task
// has been deleted, so don't dereference the pointer.
void EventLoop::taskDeleted(Task *task) {
auto p = tasks.find(task);
if (p != tasks.end()) {
err_log() << "task owned by me deleted by client!";
tasks.erase(p);
engine.deleteConnByTask(task);
}
}
bool EventLoop::startObserving(Task *from, Task *to) {
if (tasks.find(from) == tasks.end() ||
tasks.find(to) == tasks.end()) {
err_log() << "startObserving: no such task";
return false;
}
{
auto p = observing.find(to);
if (p == observing.end()) {
std::set<Task *> tset { from };
observing.insert(std::make_pair(to, tset));
} else {
p->second.insert(from);
}
}
{
auto p = observed_by.find(from);
if (p == observed_by.end()) {
std::set<Task *> tset { to };
observed_by.insert(std::make_pair(from, tset));
} else {
p->second.insert(to);
}
}
return true;
}
void EventLoop::getChildTasks(std::set<Task *> &tset, Task *parent) const {
// parent is observing its children.
auto p = observed_by.find(parent);
if (p == observed_by.end())
return;
for (auto task : p->second)
// task is observed by parent but isn't necessarily a child task.
if (tasks.at(task) == parent)
tset.insert(task);
}
void EventLoop::abortChildTasks(Task *parent) {
std::set<Task *> tset;
getChildTasks(tset, parent);
for (auto task : tset) {
task->was_killed = true;
finishedTasks.insert(task);
}
engine.yield();
}
void EventLoop::abortTask(Task *task) {
task->was_killed = true;
finishedTasks.insert(task);
engine.yield();
}
void EventLoop::wakeUpTask(Task *t) {
engine.wakeUpByTask(t);
}
std::set<Socket *> EventLoop::findConnByTask(const Task *t) const {
return engine.findSockByTask(t);
}
bool EventLoop::running(Task *task) {
return tasks.find(task) != tasks.end();
}
void EventLoop::resetTimer(Task *task, double s) {
_removeTimer(task);
if (s < 0)
return;
if (s == 0)
s = task->timerEvent();
if (s > 0) {
auto t = toUs(s);
timer_queue.insert(std::make_pair(timeNow()+t, task));
}
}
void EventLoop::_removeTimer(Task *task) {
for (auto it : timer_queue)
if (it.second == task) {
timer_queue.erase(it.first);
break;
}
}
void EventLoop::_removeTask(Task *task, bool killed) {
{
auto p = tasks.find(task);
if (p == tasks.end())
return; // Task already removed.
tasks.erase(p);
}
task->setTerminated();
task->was_killed = killed;
log() << "remove task " << task->label();
// Remove the task's connections
engine.deleteConnByTask(task);
// Remove the task's timer
for (auto it : timer_queue)
if (it.second == task) {
timer_queue.erase(it.first);
break;
}
// The dying task may be an observer:
auto p1 = observed_by.find(task);
if (p1 != observed_by.end()) {
for (auto t : p1->second) {
// task must no longer be observing t
observing[t].erase(task);
auto p = tasks.find(t);
if (p->second == task) {
// t is a child of the dying task. Mark it as an orphan:
p->second = nullptr;
if (task->kill_children)
abortTask(t);
}
}
observed_by.erase(p1);
}
// Other tasks may be observing the dying task:
auto p2 = observing.find(task);
if (p2 != observing.end()) {
for (auto t : p2->second) {
// t must no longer be observing task
observed_by[t].erase(task);
// Notify t that an observed task has died
t->taskFinished(task);
}
observing.erase(p2);
}
#ifdef USE_THREADS
if (task->is_child_thread)
finished_threads.push(task);
else
#endif
delete task;
}
Task *EventLoop::nextTimerToExecute() {
if (!timer_queue.empty() &&
timer_queue.cbegin()->first <= timeNow()) {
Task *task = timer_queue.cbegin()->second;
timer_queue.erase(timer_queue.begin());
return task;
}
return nullptr;
}
bool EventLoop::run(double timeout_s) {
TimePoint deadline = timeAfter(timeout_s);
while (true) {
// First run timers, regardless of deadline:
while (Task *task = nextTimerToExecute()) {
double ts = task->timerEvent();
if (ts > 0) {
auto t = toUs(ts);
timer_queue.insert(std::make_pair(timeNow()+t, task));
}
}
check_finished();
#ifdef USE_THREADS
if ( (tasks.empty() && threads.empty()) ||
timeNow() > deadline || aborted())
#else
if (tasks.empty() || timeNow() > deadline || aborted())
#endif
break;
double time_left;
if (timer_queue.empty()) {
time_left = secondsTo(deadline);
} else {
auto next_timer = timer_queue.cbegin()->first;
time_left = secondsTo(std::min(next_timer, deadline));
}
if (time_left <= 0) {
log() << "Timeout passed, will not poll connections";
break;
}
if (!engine.run(time_left)) {
err_log() << "fatal engine failure";
do_abort = true;
break;
}
check_finished();
}
if (do_abort)
removeAllTasks();
#ifdef USE_THREADS
return (!tasks.empty() || !threads.empty());
#else
return (!tasks.empty());
#endif
}
void EventLoop::runUntilComplete() {
while (run(1.5)) {
// for (auto &p : tasks)
// log() << "REM: " << p.first->label();
}
#ifdef USE_THREADS
dbg_log() << "End Loop; Threads left: " << threads.size();
waitForThreadsToFinish();
#endif
flushLogFile();
}
#ifdef USE_THREADS
void EventLoop::runTask(Task *task, const std::string &name,
std::ostream *log_file, EventLoop *parent) {
#else
void EventLoop::runTask(Task *task, const std::string &name,
std::ostream *log_file) {
#endif
if (log_file)
setLogFile(*log_file);
#ifdef USE_THREADS
if (task->is_child_thread)
parent = nullptr;
EventLoop loop(name, parent);
#else
EventLoop loop(name);
#endif
loop.addTask(task);
loop.runUntilComplete();
flushLogFile();
}
#ifdef USE_THREADS
void EventLoop::spawnThread(Task *task, const std::string &name,
std::ostream *log_file, Task *parent) {
if (parent_loop)
throw std::runtime_error("not in main loop");
if (parent)
threadTaskObserver.insert(std::make_pair(task, parent));
task->is_child_thread = true;
threads.insert(std::make_pair(task, std::thread(runTask, task, name,
log_file, this)));
dbg_log() << "Added thread, now have " << threads.size();
}
MsgQueue<Task *> EventLoop::finished_threads;
void EventLoop::collect_thread(Task *t) {
if (parent_loop)
return;
log() << "Thread task " << t->label() << " finished";
auto p = threads.find(t);
if (p != threads.end()) {
p->second.join();
threads.erase(p);
}
// Notify parent?
auto it = threadTaskObserver.find(t);
// TODO: make sure to erase (task, parent) from
// threadTaskObserver if parent dies first
if (it != threadTaskObserver.end()) {
Task *parent = it->second;
if (tasks.find(parent) != tasks.end())
parent->taskFinished(t);
threadTaskObserver.erase(it);
}
delete t;
}
void EventLoop::waitForThreadsToFinish() {
while (!threads.empty()) {
dbg_log() << "Threads left: " << threads.size();
Task *t = finished_threads.pop_blocking();
collect_thread(t);
}
}
#endif
void EventLoop::removeAllTasks() {
while (!tasks.empty())
_removeTask(tasks.begin()->first, true);
}
void EventLoop::check_finished() {
if (got_signal) {
int signum = got_signal;
auto p = userSignalHandler.lower_bound(signum);
auto to = userSignalHandler.upper_bound(signum);
if (p == to) {
// No user defined handler for signal, will abort
#ifdef _WIN32
err_log() << "got signal " << signum << ", will exit";
abort();
#else
if (signum == SIGPIPE) {
warn_log() << "got SIGPIPE";
} else if (signum == SIGHUP) {
Logger::reopenLogFile(openFileOnSIGHUP);
Logger::setLogLimit();
log() << "got SIGHUP";
killChildProcesses(signum);
} else if (signum == SIGTERM) {
err_log() << "got SIGTERM, exit immediately" << std::endl;
killChildProcesses(signum);
exit(0);
} else {
err_log() << "got signal " << signum << ", will exit";
killChildProcesses(signum);
abort();
}
#endif
} else {
for (; p != to; ++p) {
void (*handler)(int, EventLoop &) = p->second;
(*handler)(got_signal, *this);
}
}
got_signal = 0;
}
#ifndef _WIN32
size_t i = 0;
while (terminatedPIDtmp[i] && i<sizeof(terminatedPIDtmp)/sizeof(int)) {
int pid = terminatedPIDtmp[i], wstatus = terminatedPIDtmp[i+1];
log() << "PID " << pid << " finished, status=" << wstatus;
terminatedPIDs[pid] = wstatus;
terminatedPIDtmp[i] = 0;
i += 2;
}
while (!terminatedPIDs.empty()) {
auto it = terminatedPIDs.begin();
int pid = it->first;
int wstatus = it->second;
log() << "Terminated PID " << pid << " status " << wstatus;
terminatedPIDs.erase(it);
auto it2 = pidOwner.find(pid);
if (it2 != pidOwner.end()) {
Task *task = it2->second;
if (tasks.find(task) != tasks.end()) {
task->processFinished(pid, wstatus);
}
pidOwner.erase(it2);
}
}
#endif
#ifdef USE_THREADS
Task *t;
while (!threads.empty() && finished_threads.fetch(t))
collect_thread(t);
#endif
while (!messageTasks.empty() || !finishedTasks.empty()) {
if (!messageTasks.empty()) {
auto p = tasks.find(*messageTasks.begin());
messageTasks.erase(messageTasks.begin());
if (p != tasks.end()) {
if (Task *parent = p->second)
parent->taskMessage(p->first);
}
}
if (!finishedTasks.empty()) {
auto p = tasks.find(*finishedTasks.begin());
finishedTasks.erase(finishedTasks.begin());
if (p != tasks.end())
_removeTask(p->first);
}
}
}

308
src/framework/eventloop.h Normal file
View file

@ -0,0 +1,308 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <set>
#include <map>
#include "logger.h"
#include "engine.h"
#ifdef USE_THREADS
#include "msgqueue.h"
#endif
class Task;
class SocketConnection;
class ServerSocket;
class WorkerProcess;
/// \brief Manage timers and tasks.
///
/// This class manages Task objects and their timers. It also manages
/// all network connections through an Engine object. The Engine is an
/// "inner event loop" used to manage low-level network events.
///
/// Your code must create an EventLoop object, add one or more Task objects
/// to it, and then run the event loop, either "forever" using the method
/// EventLoop::runUntilComplete()
/// or by regularly calling the method
/// EventLoop::run(double timeout_s)
class EventLoop : public Logger {
public:
/// Create a new EventLoop. Normally, the application should have at most
/// one EventLoop in each thread.
EventLoop(std::string log_label = "MainLoop") :
Logger(log_label),
engine("NetworkEngine"),
name(log_label) {
#ifdef USE_THREADS
do_init(nullptr);
#else
do_init();
#endif
}
~EventLoop();
/// Add a task to be managed by the EventLoop. The value of the task
/// parameter must be an object of a subclass of Task. You _must_ create
/// the object with new; it cannot be an object on the stack.
/// The EventLoop will take ownership of the object and will delete it
/// when the task has finished. Before it is deleted, the parent's
/// taskFinished method will be called unless the parent is nullptr.
void addTask(Task *task, Task *parent = nullptr);
/// Run for at most timeout_s seconds.
/// Returns false if all done, otherwise true:
bool run(double timeout_s);
/// Run until all task are done.
void runUntilComplete();
#ifdef USE_THREADS
/// Create an EventLoop object that runs the task until it's finished.
/// You cannot use this if you have created your own EventLoop object.
///
/// The "parent" parameter is used if the main thread (the parent) should
/// be notified when the thread running the task is finished.
static void runTask(Task *task, const std::string &name = "MainLoop",
std::ostream *log_file = nullptr,
EventLoop *parent = nullptr);
#else
static void runTask(Task *task, const std::string &name = "MainLoop",
std::ostream *log_file = nullptr);
#endif
/// Block current thread until all spawned threads have finished.
void waitForThreadsToFinish();
/// Remove the given task.
void abortTask(Task *task);
/// Remove all tasks.
void abort() {
interrupt();
do_abort = true;
}
/// Get all tasks with the given parent.
void getChildTasks(std::set<Task *> &tset, Task *parent) const;
/// Remove all tasks with the given parent.
void abortChildTasks(Task *parent);
/// Restart idle connections owned by the given task.
void wakeUpTask(Task *t);
/// Restart an idle connection.
bool wakeUpConnection(SocketConnection *s) {
return engine.wakeUpConnection(s);
}
/// Remove a connection.
void cancelConnection(SocketConnection *s) {
engine.cancelConnection(s);
}
/// Return all connetcions owned by the given task.
std::set<Socket *> findConnByTask(const Task *t) const;
/// Return true if conn still exists.
bool isActive(const Socket *conn) const {
return engine.connActive(conn);
}
/// Remove previous timer, run after s seconds instead.
/// If s = 0, run timer immediately. If s < 0, remove timer.
void resetTimer(Task *task, double s);
/// Create a new socket connection, and add it to the loop.
/// Returns false (and deletes conn) on failure.
/// On success, returns true and calls connAdded on owner task.
/// A connection to the server will be initiated. When connected, the
/// connected() method will be called on conn to get initial state.
bool addConnection(SocketConnection *conn);
/// Use this if conn contains a socket that has already been connected.
/// Returns false (and deletes conn) on failure.
/// On success, returns true and calls connAdded on owner task,
/// then calls connected() on conn to get initial state.
bool addConnected(SocketConnection *conn);
/// Returns false (and deletes conn) on failure.
/// On success, returns true and calls serverAdded on owner task.
bool addServer(ServerSocket *conn);
#ifdef USE_GNUTLS
/// Use SSL certificate for a listening socket.
bool tlsSetKey(ServerSocket *conn, const std::string &crt_path,
const std::string &key_path, const std::string &password) {
return engine.tlsSetKey(conn, crt_path, key_path, password);
}
/// Set path to file containing chain of trust for SSL certificate.
bool setCABundle(const std::string &path) {
return engine.setCABundle(path);
}
#endif
/// Return true if task is running.
bool running(Task *task);
/// \brief
/// Notify EventLoop that a task object it ows has been deleted.
///
/// This is just a safeguard against buggy clients.
/// Only the EventLoop is allowed to delete tasks is owns.
void taskDeleted(Task *task);
/// Call this to make the network engine yield control
/// to me (the task supervisor).
static void interrupt() {
Engine::yield();
}
#ifdef USE_THREADS
/// Create a new thread and run task in its own loop in that thread.
void spawnThread(Task *task, const std::string &name="ThreadLoop",
std::ostream *log_file = nullptr,
Task *parent = nullptr);
#endif
#ifdef _WIN32
int externalCommand(Task *owner, const char *const argv[]) {
// Not implemented
exit(1);
}
#else
/// Asynchronously execute an external command.
/// Return false on immediate failure.
int externalCommand(Task *owner, const char *const argv[]);
/// Fork into background, detach from shell.
static void daemonize();
/// Create a child process. Return child's PID. Channels can be
/// used to pass sockets and messages between parent and child.
WorkerProcess *createWorker(Task *parent, std::ostream *log_file,
unsigned int channels, unsigned int wno);
/// Create a child process. Return child's PID. Channels can be
/// used to pass sockets and messages between parent and child.
WorkerProcess *createWorker(Task *parent, const std::string &log_file_name,
unsigned int channels, unsigned int wno);
/// Send signal to all child processes
void killChildProcesses(int signum);
/// \brief
/// Set path to log file.
///
/// The new log file will be activated upon receiving
/// the SIGHUP signal.
static void setLogFilename(const std::string &filename) {
openFileOnSIGHUP = filename;
}
#endif
/// \brief
/// Mark the given task as finished.
///
/// Don't call this method directly. Use Task::setResult instead.
void notifyTaskFinished(Task *task) {
auto ret = finishedTasks.insert(task);
if (ret.second)
engine.yield();
}
/// \brief
/// Notify the EventLoop that the given task has a message to deliver.
///
/// Don't call this method directly. Use Task::setMessage instead.
void notifyTaskMessage(Task *task) {
auto ret = messageTasks.insert(task);
if (ret.second)
engine.yield();
}
/// Return true if the EventLoop is about to be terminated.
bool aborted() const {
return do_abort;
}
/// Add handler for the given OS signal.
void addSignalHandler(int signum, void (*handler)(int, EventLoop &));
/// Enable events from task "from" to task "to". I.e. "from" will be able to
/// call executeHandler with "to" as a parameter. If "to" dies before
/// "from", "from" will be notified through a call to taskFinished.
/// Will return false unless both tasks still exist.
bool startObserving(Task *from, Task *to);
/// Return true if observer is observing task.
bool isObserving(Task *observer, Task *task) const {
auto p = observed_by.find(observer);
return p != observed_by.end() &&
p->second.find(task) != p->second.end();
}
private:
#ifndef _WIN32
#ifdef USE_THREADS
thread_local
#endif
static std::map<int, int> terminatedPIDs;
std::map<int, Task *> pidOwner;
#ifdef USE_THREADS
thread_local
#endif
static std::string openFileOnSIGHUP;
#endif
#ifdef USE_THREADS
void do_init(EventLoop *parent);
EventLoop(std::string log_label, EventLoop *parent) :
Logger(log_label),
engine("NetworkEngine"),
name(log_label) {
do_init(parent);
}
std::map<Task *, std::thread> threads;
static MsgQueue<Task *> finished_threads;
std::map<Task *, Task *> threadTaskObserver;
void collect_thread(Task *t);
EventLoop *parent_loop;
#else
void do_init();
#endif
#ifdef USE_THREADS
//thread_local
#endif
static volatile int got_signal;
#ifdef USE_THREADS
thread_local
#endif
static volatile int terminatedPIDtmp[100];
static void signalHandler(int signum);
void removeAllTasks();
void check_finished();
Task *nextTimerToExecute();
void _removeTimer(Task *task);
void _removeTask(Task *task, bool killed = false);
Engine engine;
// Map each task to its parent (or, if it has no parent, to nullptr)
std::map<Task *, Task *> tasks;
// These are used to keep track of "observation" between tasks.
// E.g. a parent task is observing its children.
std::map<Task *, std::set<Task *> > observed_by;
std::map<Task *, std::set<Task *> > observing;
std::set<Task *> finishedTasks, messageTasks;
std::multimap<int, void (*)(int, EventLoop &)> userSignalHandler;
std::multimap<TimePoint, Task *> timer_queue;
std::string name;
bool do_abort = false;
};

View file

@ -0,0 +1,161 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <signal.h>
#include "loadbalancer.h"
#include "socketreceiver.h"
#include "workerprocess.h"
LoadBalancer::LoadBalancer(const TaskConfig &tc) :
Task("LoadBalancer"), my_config(tc) {
try {
tot_no_workers = std::stoul(my_config.value("workers"));
} catch (...) {
tot_no_workers = 0;
}
auto range = tc.cfg().equal_range("workercfg");
for (auto p=range.first; p!=range.second; ++p)
(worker_config += p->second) += "\n";
}
LoadBalancer::~LoadBalancer() {
for (WorkerProcess *wp : worker_proc)
if (wp)
delete wp;
}
#ifdef USE_GNUTLS
bool LoadBalancer::tlsSetKey(ServerSocket *conn, const std::string &crt_path,
const std::string &key_path,
const std::string &password) {
portMap[conn] = no_channels;
worker_config += "channel" + std::to_string(no_channels) + " tls " +
crt_path + " " + key_path + " " + password + "\n";
++no_channels;
return true;
}
#endif
double LoadBalancer::start() {
if (!tot_no_workers) {
setError("Internal error, no workers");
return 0.0;
}
worker_proc.resize(tot_no_workers);
worker_proc_health.resize(tot_no_workers);
if (parseListen(my_config, "LoadBalancerSocket")) {
for (size_t i=0; i<tot_no_workers; ++i)
new_worker(i);
} else {
setResult("Failed, cannot start server");
}
return 0.0;
}
SocketConnection *LoadBalancer::newClient(int fd, const char *, uint16_t,
ServerSocket *srv) {
doPass(fd, nextWorker(), srv);
rotateNextWorker();
return nullptr;
}
void LoadBalancer::serverRemoved(ServerSocket *s) {
log() << "Server removed socket " << s;
if (SocketReceiver *conn = dynamic_cast<SocketReceiver *>(s)) {
if (pid_t peer = conn->peerPid()) {
removeWorker(peer);
log() << "Kill process " << peer;
kill(peer, SIGKILL);
}
} else {
log() << "Not a receiver";
}
}
#ifdef USE_GNUTLS
void LoadBalancer::doPass(int fd, size_t wid, ServerSocket *srv) {
#else
void LoadBalancer::doPass(int fd, size_t wid, ServerSocket *) {
#endif
if (wid >= worker_proc.size() || !worker_proc[wid]) {
err_log() << "Worker " << wid << " dead, dropping socket " << fd;
return;
}
#ifdef USE_GNUTLS
unsigned int ch = portMap.find(srv) != portMap.end() ? portMap[srv] : 0;
#else
unsigned int ch = 0;
#endif
SocketReceiver *channel = worker_proc[wid]->channel(ch);
dbg_log() << "New connection fd=" << fd << " pass to worker " << wid;
int ret = channel->passSocketToPeer(fd);
if (!ret) {
worker_proc_health[wid] = TimePoint();
} else if (ret == EAGAIN) {
// The peer might be broken. Or we have a burst of new connections.
// If the peer stays broken for more than 5 seconds, we'll kill it.
if (worker_proc_health[wid] == TimePoint()) {
warn_log() << "Worker " << wid << " not responding, give it 5s to recover.";
worker_proc_health[wid] = timeNow();
} else if (secondsSince(worker_proc_health[wid]) > 5) {
err_log() << "job queue full, worker " << wid << " will be restarted";
removeWorker(worker_proc[wid]->pid());
worker_proc_health[wid] = TimePoint();
} else {
err_log() << "worker " << wid << " busy, cannot pass socket";
}
} else {
err_log() << "cannot pass socket to worker " << wid << " " << strerror(ret);
}
}
void LoadBalancer::processFinished(int pid, int wstatus) {
log() << "End of PID " << pid << ", status " << wstatus;
removeWorker(pid);
}
void LoadBalancer::new_worker(size_t i) {
if (worker_proc[i]) {
delete worker_proc[i];
worker_proc[i] = nullptr;
}
if (terminated())
return;
std::string logfilename = my_config.value("workerlog");
if (logfilename.empty()) {
worker_proc[i] = createWorker(nullptr, no_channels,
static_cast<unsigned int>(i));
} else {
// In logfilename, replace all %d with worker number (i.e. i),
// adjusting to fixed width by filling with zeroes.
auto len = std::to_string(tot_no_workers).size();
auto wno = std::to_string(i);
wno = std::string(len-wno.size(), '0') + wno;
while (true) {
auto pos = logfilename.find("%d");
if (pos == std::string::npos)
break;
logfilename.replace(pos, 2, wno);
}
worker_proc[i] = createWorker(logfilename, no_channels,
static_cast<unsigned int>(i));
}
if (worker_proc[i])
log() << "Created worker " << i << ", pid: " << worker_proc[i]->pid();
}
void LoadBalancer::removeWorker(pid_t pid) {
for (size_t i=0; i<worker_proc.size(); ++i)
if (worker_proc[i] && worker_proc[i]->pid() == pid) {
if (max_retries) {
--max_retries;
} else {
setResult("Too many failures");
}
new_worker(i);
break;
}
}

View file

@ -0,0 +1,78 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include "task.h"
#include "socketreceiver.h"
#include "workerprocess.h"
/// \brief
/// Create worker (child) processes, and pass new connections
/// evenly among them.
///
/// New clients are passed to the worker processes using a round-robin
/// algorithm. Override the Task::newClient method to use a more
/// sophisticated algorithm.
class LoadBalancer : public Task {
public:
LoadBalancer(const TaskConfig &tc);
~LoadBalancer() override;
SocketConnection *newClient(int fd, const char *, uint16_t,
ServerSocket *) override;
double start() override;
#ifdef USE_GNUTLS
bool tlsSetKey(ServerSocket *conn, const std::string &crt_path,
const std::string &key_path,
const std::string &password) override;
#endif
void serverRemoved(ServerSocket *s) override;
void processFinished(int pid, int wstatus) override;
protected:
/// Return a worker number.
size_t nextWorker() const {
return next_worker;
}
/// Move on to next worker (in a round-robin fashion) for
/// LoadBalancer::nextWorker.
void rotateNextWorker() {
if (++next_worker >= worker_proc.size())
next_worker = 0;
}
/// Pass a connection to a worker process.
void doPass(int fd, size_t wid, ServerSocket *srv);
/// Create a new worker process.
void new_worker(size_t i);
/// Remove a worker process.
void removeWorker(pid_t pid);
/// Return configuration of a worker process.
std::string workerConfig(unsigned int i=0) const {
return worker_config + "\nworker_number " + std::to_string(i);
}
/// Max number of times to restart failed worker processes.
void setMaxRetries(unsigned int n) {
max_retries = n;
}
private:
#ifdef USE_GNUTLS
std::map<ServerSocket *, unsigned int> portMap;
#endif
unsigned int max_retries = 100;
unsigned int no_channels = 1;
std::vector<WorkerProcess *> worker_proc;
std::vector<TimePoint> worker_proc_health;
size_t next_worker = 0;
TaskConfig my_config;
std::string worker_config;
size_t tot_no_workers;
};

179
src/framework/logger.cpp Normal file
View file

@ -0,0 +1,179 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <iostream>
#include <fstream>
#include "logger.h"
#include <iomanip>
#include <chrono>
#include <random>
#ifdef _WIN32
#include <winsock2.h>
#endif
double Logger::secondsSince(const TimePoint &t) {
auto now = timeNow();
std::chrono::duration<double> d = now - t;
return d.count();
}
double Logger::secondsTo(const TimePoint &t) {
auto now = timeNow();
std::chrono::duration<double> d = t - now;
return d.count();
}
int64_t Logger::msSince(const TimePoint &t) {
auto now = timeNow();
return std::chrono::duration_cast<std::chrono::milliseconds>
(now - t).count();
}
int64_t Logger::msTo(const TimePoint &t) {
auto now = timeNow();
return std::chrono::duration_cast<std::chrono::milliseconds>
(t - now).count();
}
std::string Logger::dateString(time_t t) {
// put_time might not be available in older libs.
char dstr[200];
if (!t)
t = time(nullptr);
struct tm *dtm = localtime(&t);
size_t len = strftime(dstr, sizeof(dstr), "%FT%T%z", dtm);
return std::string(dstr, len);
}
std::string Logger::dateString2(time_t t) {
// put_time might not be available in older libs.
char dstr[200];
if (!t)
t = time(nullptr);
struct tm *dtm = localtime(&t);
size_t len = strftime(dstr, sizeof(dstr), "%a, %d %b %Y %H:%M:%S", dtm);
return std::string(dstr, len);
}
std::string Logger::createHashKey(unsigned int length) {
std::random_device rng;
std::ostringstream s;
std::uniform_int_distribution<unsigned short> dist;
while (s.str().size() < length)
s << std::setw(sizeof(unsigned short)*2) << std::uppercase
<< std::hex << std::setfill('0') << dist(rng);
return s.str();
}
#ifdef USE_THREADS
thread_local
#endif
bool Logger::in_error = false;
#ifdef USE_THREADS
thread_local
#endif
TimePoint Logger::global_start_time(timeNow());
#ifdef USE_THREADS
thread_local
#endif
std::ostream *Logger::_logFile = &std::cerr;
#ifdef USE_THREADS
thread_local
#endif
unsigned int Logger::log_count = 100000;
#ifdef USE_THREADS
thread_local
#endif
unsigned int Logger::warn_count = 10000;
#ifdef USE_THREADS
thread_local
#endif
unsigned int Logger::err_count = 10000;
#ifdef USE_THREADS
thread_local
#endif
unsigned int Logger::log_count_saved = 100000;
#ifdef USE_THREADS
thread_local
#endif
unsigned int Logger::warn_count_saved = 10000;
#ifdef USE_THREADS
thread_local
#endif
unsigned int Logger::err_count_saved = 10000;
DummyStream Logger::_dummyLog;
#ifdef USE_THREADS
thread_local
#endif
std::ostringstream Logger::_blackHole;
DummyStream::~DummyStream() {
}
void Logger::setLogLimit(unsigned int loglines, unsigned int warnlines,
unsigned int errlines) {
if (loglines)
log_count_saved = loglines;
if (warnlines)
warn_count_saved = warnlines;
if (errlines)
err_count_saved = errlines;
log_count = log_count_saved;
warn_count = warn_count_saved;
err_count = err_count_saved;
}
void Logger::sayTime(std::ostream &stream) {
std::time_t t = std::time(nullptr);
std::tm *tm = std::localtime(&t);
stream << tm->tm_year+1900 << '-'
<< std::setfill('0') << std::setw(2) << tm->tm_mon+1 << '-'
<< std::setfill('0') << std::setw(2) << tm->tm_mday << ' '
<< std::setfill('0') << std::setw(2) << tm->tm_hour << ':'
<< std::setfill('0') << std::setw(2) << tm->tm_min << ':'
<< std::setfill('0') << std::setw(2) << tm->tm_sec;
}
#ifdef TASKRUNNER_LOGERR
std::ostream &Logger::errno_log() const {
if (err_count) {
in_error = true;
--err_count;
*_logFile << "\n" << global_elapsed_ms() << ' ' << _label << "*** "
<< (err_count ? "ERROR ***: " : "LAST ERR ***: ")
#ifdef _WIN32
<< std::to_string(WSAGetLastError())
#else
<< strerror(errno)
#endif
<< ": ";
return *_logFile;
} else {
return _blackHole;
}
}
#else
DummyStream &Logger::errno_log() const {
return _dummyLog;
}
#endif
void Logger::setLogFile(std::ostream &stream) {
_logFile = &stream;
in_error = false;
global_start_time = timeNow();
sayTime(stream);
}
void Logger::reopenLogFile(const std::string &filename) {
if (std::ofstream *the_log = dynamic_cast<std::ofstream *>(_logFile)) {
*the_log << "\n";
the_log->close();
the_log->open(filename, std::ios::app);
}
}

382
src/framework/logger.h Normal file
View file

@ -0,0 +1,382 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifdef max
#undef max
#endif
#else
#include <string.h>
#endif
#ifndef DEBUG
#define DEBUG 0
#endif
#ifndef TARGET_OS_IPHONE
#define TARGET_OS_IPHONE 0
#endif
#include <iostream>
#include <sstream>
#include <chrono>
#include <thread>
#ifdef __ANDROID_API__
#include <android/log.h>
#endif
/// This class is used to optionally disable logging operations at compile time.
class DummyStream {
public:
/// Do nothing, emulating the << stream operator.
template<class T>
DummyStream &operator<<(T ) { return *this; }
#ifdef __ANDROID_API__
DummyStream &operator<<(const char *s) {
__android_log_print(ANDROID_LOG_VERBOSE, "BBK", "%s", s);
return *this; }
DummyStream &operator<<(std::string s) {
__android_log_print(ANDROID_LOG_VERBOSE, "BBK", "%s", s.c_str());
return *this; }
DummyStream &operator<<(int i) {
__android_log_print(ANDROID_LOG_VERBOSE, "BBK", "%d", i);
return *this; }
DummyStream &operator<<(double x) {
__android_log_print(ANDROID_LOG_VERBOSE, "BBK", "%f", x);
return *this; }
#endif
/// Do nothing, emulating the << stream operator.
DummyStream& operator<<(std::ostream &(*)(std::ostream &) ) {
return *this;
}
virtual ~DummyStream();
private:
};
/// \class TimePoint
/// \brief
/// Measure elapsed time during execution,
/// for example by timer events.
///
/// It is simply a typedef to std::chrono::steady_clock::time_point.
///
/// Example:
///
/// TimePoint start = Logger::timeNow();
/// // Do stuff...
/// std::cout << Logger::secondsSince(start) << " seconds have elapsed.";
/// // 2.00042 seconds have elapsed.
typedef std::chrono::steady_clock::time_point TimePoint;
/// \brief
/// A simple logger. All classes that want to write to the global log
/// file should inherit from this class.
///
/// By default, logs will be written to cerr. To log elsewhere, you must
/// call the static function Logger::setLogFile with a stream object (e.g. an
/// std::ofstream or an std::ostringstream) which the logs will be written to.
/// The stream will be used globally. You must make sure the global stream
/// is never destroyed, at least not before Logger::setLogFile has been called
/// with another stream.
class Logger {
public:
/// Each object of the Logger class (or its subclasses) have a log label,
/// which will often be thought of as the name of the object.
Logger(std::string label) :
_label(label) {
// TODO: single initialisation
_blackHole.clear(std::istream::eofbit);
}
/// \brief Set global log destination.
///
/// The given stream will be the destination of all subsequent log calls
/// globally. You must make sure the global stream never is destroyed, at
/// least not until Logger::setLogFile is called with another stream.
static void setLogFile(std::ostream &stream);
/// If current log is a file (ofstream), reopen it with new filename:
static void reopenLogFile(const std::string &filename);
/// \brief Set max number of lines of info/warn/err log.
///
/// If 0, reset to previous (non-zero) max number of lines.
/// After the limit has been reached, no more lines for that log level
/// will be printed until the limit has been reset.
static void setLogLimit(unsigned int loglines = 0,
unsigned int warnlines = 0,
unsigned int errlines = 0);
/// Write current local time to the given stream
static void sayTime(std::ostream &stream);
/// Return true if any error has been logged (globally since start)
static bool inError() {
return in_error;
}
/// \brief Write a line of error log.
///
/// Access the current global error log stream. A line feed and a preamble
/// will be written to the stream. Then send whetever you want to the log
/// stream using the standard std::ostream API.
///
/// In non-static members of subclasses to Logger, the method
/// Logger::err_log() should be used instead of this function.
static std::ostream &err_log(const std::string &label) {
if (err_count) {
in_error = true;
--err_count;
*_logFile << "\n" << global_elapsed_ms() << ' ' << label << " *** "
<< (err_count ? "ERROR ***: " : "LAST ERR ***: ");
return *_logFile;
} else {
return _blackHole;
}
}
/// \brief Write a line of warning log.
///
/// Access the current global warning log stream. A line feed and a preamble
/// will be written to the stream. Then send whetever you want to the log
/// stream using the standard std::ostream API.
///
/// In non-static members of subclasses to Logger, the method
/// Logger::warn_log() should be used instead of this function.
static std::ostream &warn_log(const std::string &label) {
if (warn_count) {
--warn_count;
*_logFile << "\n" << global_elapsed_ms() << ' ' << label << " *** "
<< (warn_count ? "WARNING ***: " : "LAST WARN ***: ");
return *_logFile;
} else {
return _blackHole;
}
}
/// \brief Write a line of info log.
///
/// Access the current global info log stream. A line feed and a preamble
/// will be written to the stream. Then send whetever you want to the log
/// stream using the standard std::ostream API.
///
/// In non-static members of subclasses to Logger, the method
/// Logger::warn_log() should be used instead of this function.
static std::ostream &log(const std::string &label) {
if (log_count) {
--log_count;
*_logFile << "\n" << global_elapsed_ms() << ' ' << label << ": ";
if (!log_count)
*_logFile << "LAST LOG: ";
return *_logFile;
} else {
return _blackHole;
}
}
/// Anything written to the global log may be buffered for quite some time,
/// and thus not visible in the destination file. This method will flush
/// the buffer and write an extra empty line.
///
/// Calling this often may be bad for performance.
static void flushLogFile() {
*_logFile << std::endl;
}
/// Disable all log output until next call to Logger::setLogFile.
static void pauseLogging() {
_logFile = &_blackHole;
}
/// Return number of seconds since the given TimePoint.
/// The returned value might be negative.
static double secondsSince(const TimePoint &t);
/// Return number of seconds until the given TimePoint.
/// The returned value might be negative.
static double secondsTo(const TimePoint &t);
/// Return number of milliseconds since the given TimePoint.
/// The returned value might be negative.
static int64_t msSince(const TimePoint &t);
/// Return number of milliseconds until the given TimePoint.
/// The returned value might be negative.
static int64_t msTo(const TimePoint &t);
/// Return true if current time is after the given TimePoint.
static bool hasExpired(const TimePoint &t) {
return secondsSince(t) >= 0;
}
/// Return current time.
static TimePoint timeNow() {
return std::chrono::steady_clock::now();
}
/// Return current time plus s seconds.
static TimePoint timeAfter(double s) {
return timeNow() + std::chrono::microseconds(toUs(s));
}
/// Return a very distant time.
static TimePoint timeMax() {
return TimePoint::max();
}
/// Convert s (seconds) to std::chrono::microseconds
static std::chrono::microseconds toUs(double t) {
auto us = static_cast<std::chrono::microseconds::rep>(1e6*t);
return std::chrono::microseconds(us);
}
/// Return local time, formatted as 2023-10-14T09:38:47+0200
static std::string dateString(time_t t = 0);
/// Return local time, formatted as Sat, 14 Oct 2023 09:38:47
static std::string dateString2(time_t t = 0);
/// \brief Return a random string.
///
/// Create string of length random hex chars from system's random number
/// generator. The length should be a multiple of 4.
static std::string createHashKey(unsigned int length = 20);
/// Return the object's log label.
std::string label() const {
return _label;
}
/// Modify the object's log label
void resetLabel(const std::string &new_label) {
_label = new_label;
}
protected:
#if DEBUG
#define TASKRUNNER_LOGERR
#define TASKRUNNER_LOGWARN
#define TASKRUNNER_LOGINFO
#define TASKRUNNER_LOGBDG
#endif
#ifdef TASKRUNNER_LOGERR
/// \brief Write a line of error log after a failed system call
/// has set the global errno to a non-zero value.
///
/// Access the current global error log stream. A line feed and a preamble,
/// including the latest OS error, will be written to the stream.
///
/// *Note!* The global error stream will be "disabled" (i.e. set to a dummy stream)
/// unless compiler macro TASKRUNNER_LOGERR is defined.
std::ostream &errno_log() const;
/// \brief Write a line of error log.
///
/// Access the current global error log stream. A line feed and a preamble
/// will be written to the stream. Then send whetever you want to the log
/// stream using the standard std::ostream API.
///
/// *Note!* The global error log stream will be "disabled" (i.e. set to a
/// dummy stream) unless compiler macro TASKRUNNER_LOGERR is defined.
///
/// May be used in any non-static member of any subclass. Example:
///
/// err_log() << "Child task " << t->label() << " failed.";
std::ostream &err_log() const {
return err_log(_label);
}
#else
DummyStream &errno_log() const;
static DummyStream &err_log() {
return _dummyLog;
}
#endif
#ifdef TASKRUNNER_LOGWARN
/// \brief Write a line of warning log.
///
/// Access the current global warning log stream. A line feed and a
/// preamble will be written to the stream. Then send whetever you want to
/// the log stream using the standard std::ostream API.
///
/// *Note!* The global warning log stream will be "disabled" (i.e. set to a
/// dummy stream) unless compiler macro TASKRUNNER_LOGWARN is defined.
std::ostream &warn_log() const {
return warn_log(_label);
}
#else
static DummyStream &warn_log() {
return _dummyLog;
}
#endif
#ifdef TASKRUNNER_LOGINFO
/// \brief Write a line of info log.
///
/// Access the current global info log stream. A line feed and a preamble
/// will be written to the stream. Then send whetever you want to the log
/// stream using the standard std::ostream API.
///
/// *Note!* The global info log stream will be "disabled" (i.e. set to a
/// dummy stream) unless compiler macro TASKRUNNER_LOGINFO is defined.
std::ostream &log() const {
return log(_label);
}
#else
static DummyStream &log() {
return _dummyLog;
}
#endif
#ifdef TASKRUNNER_LOGDBG
/// \brief Write a line of debug log.
///
/// Access the current global debug log stream. A line feed and a preamble
/// will be written to the stream. Then send whetever you want to the log
/// stream using the standard std::ostream API.
///
/// *Note!* The global debug log stream will be "disabled" (i.e. set to a
/// dummy stream) unless compiler macro TASKRUNNER_LOGDBG is defined.
std::ostream &dbg_log() const {
*_logFile << "\n" << global_elapsed_ms() << ' ' << _label << ": ";
return *_logFile;
}
#else
static DummyStream &dbg_log() {
return _dummyLog;
}
#endif
private:
static int64_t global_elapsed_ms() {
return msSince(global_start_time);
}
std::string _label;
#ifdef USE_THREADS
thread_local
#endif
static bool in_error;
#ifdef USE_THREADS
thread_local
#endif
static TimePoint global_start_time;
#ifdef USE_THREADS
thread_local
#endif
static std::ostream *_logFile;
#ifdef USE_THREADS
thread_local
#endif
static std::ostringstream _blackHole;
#ifdef USE_THREADS
thread_local
#endif
static unsigned int log_count, warn_count, err_count,
log_count_saved, warn_count_saved, err_count_saved;
static DummyStream _dummyLog;
};

140
src/framework/mk.inc Normal file
View file

@ -0,0 +1,140 @@
ALL += $(TARGET) $(PROGRAMS)
all: $(ALL)
OS:=$(shell uname)
CXXFLAGS += -O2 -W -Wall -I$(DIRLEVEL)
GLIB_COMPILE_RESOURCES = glib-compile-resources
ifeq ($(OS),Linux)
CXX = g++
endif
ifeq ($(OS),Darwin)
CXX = c++
CXXFLAGS += -x objective-c++
LIBS += -framework Foundation -framework ApplicationServices
endif
ifeq ($(OS),OpenBSD)
#CXX = ec++
CXX = clang++
endif
ifeq ($(OS),FreeBSD)
CXX = c++
endif
ifeq ($(OS),NetBSD)
CXX = clang++
CXXFLAGS += -I/usr/pkg/include
LIBS += -Wl,-R/usr/pkg/lib -L/usr/pkg/lib
endif
CXXFLAGS += -std=c++14
LINK ?= $(CXX)
ifeq ($(STATIC),1)
LDFLAGS += -static-libstdc++
endif
ifeq ($(STATIC),2)
LDFLAGS += -static -static-libstdc++
endif
ifneq ($(findstring clang,$(CXX)),)
CXXFLAGS += -Weverything -Wno-c++98-compat -Wno-exit-time-destructors \
-Wno-global-constructors -Wno-padded -Wno-disabled-macro-expansion \
-Wno-float-equal
endif
ifeq ($(CXX),g++)
CXXFLAGS += --pedantic -Wextra
endif
ifeq ($(GNUTLS),1)
CXXFLAGS += -DUSE_GNUTLS -I/usr/local/include
LIBS += -L/usr/local/lib -lgnutls
endif
ifeq ($(THREADS),1)
CXXFLAGS += -DUSE_THREADS
LIBS += -pthread
endif
ifeq ($(LOGLEVEL),)
LOGLEVEL=info
endif
ifeq ($(LOGLEVEL),err)
CXXFLAGS += -DTASKRUNNER_LOGERR
endif
ifeq ($(LOGLEVEL),warn)
CXXFLAGS += -DTASKRUNNER_LOGWARN -DTASKRUNNER_LOGERR
endif
ifeq ($(LOGLEVEL),info)
CXXFLAGS += -DTASKRUNNER_LOGERR -DTASKRUNNER_LOGWARN -DTASKRUNNER_LOGINFO
endif
ifeq ($(LOGLEVEL),dbg)
CXXFLAGS += -DTASKRUNNER_LOGERR -DTASKRUNNER_LOGWARN
CXXFLAGS += -DTASKRUNNER_LOGINFO -DTASKRUNNER_LOGDBG
endif
ifeq ($(SANDBOXED),1)
CXXFLAGS += -DIS_SANDBOXED
endif
SOURCES += \
$(DIRLEVEL)/framework/task.cpp \
$(DIRLEVEL)/framework/taskconfig.cpp \
$(DIRLEVEL)/framework/engine.cpp \
$(DIRLEVEL)/framework/eventloop.cpp \
$(DIRLEVEL)/framework/socket.cpp \
$(DIRLEVEL)/framework/socketconnection.cpp \
$(DIRLEVEL)/framework/serversocket.cpp \
$(DIRLEVEL)/framework/socketreceiver.cpp \
$(DIRLEVEL)/framework/logger.cpp \
$(DIRLEVEL)/framework/bridgetask.cpp \
$(DIRLEVEL)/framework/synchronousbridge.cpp \
$(DIRLEVEL)/json11/json11.cpp
OPT_SOURCES += \
$(DIRLEVEL)/framework/shortmessageconnection.cpp \
$(DIRLEVEL)/framework/threadbridge.cpp \
$(DIRLEVEL)/framework/unixdomainbridge.cpp \
$(DIRLEVEL)/framework/unixdomainclient.cpp \
$(DIRLEVEL)/framework/loadbalancer.cpp
OBJ=$(SOURCES:.cpp=.o)
EXTRA_OBJ=$(EXTRA_SOURCES:.cpp=.o)
OPT_OBJ=$(OPT_SOURCES:.cpp=.o)
RC_OBJ=$(RC_SOURCES:.rc.xml=.rc.o)
RC_DEPS=$(RC_SOURCES:.rc.xml=.rc_d)
RC_DEPS+=$(RC_SOURCES:.rc.xml=.rc.d)
RC_INTERMEDIATE+=$(RC_SOURCES:.rc.xml=.rc.cpp)
%.d: %.cpp
$(CXX) -MM $(CXXFLAGS) -MT $(@:.d=.o) $< > $@
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
%.rc_d: %.rc.xml
$(GLIB_COMPILE_RESOURCES) --generate-dependencies --dependency-file=$@ $<
%.rc.cpp: %.rc.xml
$(GLIB_COMPILE_RESOURCES) --generate-source $< --target=$@
$(TARGET): $(OBJ) $(RC_OBJ)
$(CXX) $(LDFLAGS) -o $@ $^ $(LIBS)
$(PROGRAMS):
$(CXX) $(LDFLAGS) -o $@ $^ $(LIBS)
ifneq ($(MAKECMDGOALS),clean)
include $(SOURCES:.cpp=.d) $(RC_DEPS) $(EXTRA_SOURCES:.cpp=.d)
endif
clean:
$(RM) $(CLEAN) $(TARGET) $(OBJ) $(OPT_OBJ) $(EXTRA_OBJ) *~ \
$(SOURCES:.cpp=.d) $(EXTRA_SOURCES:.cpp=.d) \
$(RC_OBJ) $(RC_DEPS) $(RC_INTERMEDIATE) $(PROGRAMS)

72
src/framework/msgqueue.h Normal file
View file

@ -0,0 +1,72 @@
#pragma once
#include <queue>
#include <mutex>
#include <condition_variable>
/// \brief
/// Thread safe queue.
///
/// By design, there is no method named `pop`.
/// To retrieve an object from the queue, you must
/// use either the non-blocking MsgQueue::fetch
/// or the blocking MsgQueue::pop_blocking method.
template <class T>
class MsgQueue {
public:
MsgQueue() :
queue(),
mutex(),
cond() {
}
~MsgQueue() {
}
/// Add object at the end of the queue.
void push(T t) {
std::lock_guard<std::mutex> lock(mutex);
queue.push(t);
cond.notify_one();
}
/// Return true if the queue is empty.
bool empty() {
std::unique_lock<std::mutex> lock(mutex);
return queue.empty();
}
/// \brief
/// Wait until there is an object in the queue, then
/// remove and return the first object.
T pop_blocking() {
std::unique_lock<std::mutex> lock(mutex);
while (queue.empty()) {
cond.wait(lock);
}
T val = queue.front();
queue.pop();
return val;
}
/// \brief
/// A non-blocking pop.
///
/// If the queue is empty, return false.
/// Otherwise remove the first object from the queue,
/// assign it to `val`, and return true.
bool fetch(T &val)
{
std::unique_lock<std::mutex> lock(mutex);
if (queue.empty())
return false;
val = queue.front();
queue.pop();
return true;
}
private:
std::queue<T> queue;
mutable std::mutex mutex;
std::condition_variable cond;
};

25
src/framework/pollstate.h Normal file
View file

@ -0,0 +1,25 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
/*! \file */
/// \enum PollState
/// After doing an operation on a socket, a PollState must be returned to the
/// network engine to describe what you want it to do next with the socket.
enum class PollState {
NONE, /**< Do nothing right now, but keep socket open for later. */
READ_BLOCKED, /**< Don't check for incoming data/close, but check for
writability if wantToSend(). */
CONNECTING, /**< Wait for asynchronous connect to complete. */
#ifdef USE_GNUTLS
TLS_HANDSHAKE, /**< Wait for TLS handshake to complete. */
#endif
CLOSE, /**< Close the socket gracefully. */
KEEPALIVE, /**< Put the connected socket in keep-alive cache. */
KILL, /**< Terminate connection immediately, discarding buffers. */
READ, /**< Check for incoming data/close. */
WRITE, /**< Check for close, and for writability. */
READ_WRITE /**< Check for incoming data/close, and for writability. */
};

View file

@ -0,0 +1,51 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#ifdef _WIN32
#include <WS2tcpip.h>
#else
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include "serversocket.h"
class Task;
ServerSocket::ServerSocket(const std::string &label, Task *task,
uint16_t port, const std::string &ip) :
Socket(label, task, ip, port) {
}
ServerSocket::~ServerSocket() {
}
SocketConnection *ServerSocket::incoming() {
struct sockaddr_storage remoteaddr; // client address
socklen_t addrlen = sizeof(remoteaddr);
int fd = accept(socket(),
reinterpret_cast<sockaddr *>(&remoteaddr), &addrlen);
if (fd < 0) {
if (errno == EMFILE)
Engine::notifyOutOfFds();
errno_log() << "accept failure on socket " << socket();
return nullptr;
}
if (!setNonBlocking(fd)) {
log() << "cannot set non-blocking " << fd;
closeSocket(fd);
return nullptr;
}
uint16_t port;
const char *ip = getIp(reinterpret_cast<sockaddr *>(&remoteaddr), &port);
log() << "Incoming socket " << fd << " from " << ip << " port " << port;
SocketConnection *conn = owner()->newClient(fd, ip, port, this);
if (!conn)
closeSocket(fd);
return conn;
}

View file

@ -0,0 +1,58 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include "socket.h"
#include "task.h"
class SocketConnection;
/// \brief
/// Listen on a single socket for incoming connections.
///
/// The owner task decides what to do when someone tries to connect.
class ServerSocket : public Socket {
public:
/// \brief
/// Create a new server socket.
///
/// Will listen on the given ip address and port number.
///
/// *Note!* If ip is an empty string, the socket will listen
/// on all local IPv4 and IPv6 addresses.
ServerSocket(const std::string &label, Task *task,
uint16_t port, const std::string &ip = "127.0.0.1");
/// \brief
/// Create a new server to listen on an existing file descriptor.
ServerSocket(int fd, const std::string &label, Task *owner) :
Socket(label, owner, fd) {
}
virtual ~ServerSocket() override;
/// Server sockets shall not be cached.
std::string cacheLabel() override {
return std::string();
}
/// Schedule listen socket for removal.
void stopListening() {
closeMe();
}
/// Return Connection object if new client available, else return nullptr.
virtual SocketConnection *incoming();
#ifdef USE_GNUTLS
void tlsSetKey(unsigned int i) {
tlsKeyIndex = i;
}
unsigned int tlsKey() const {
return tlsKeyIndex;
}
private:
unsigned int tlsKeyIndex = 0;
#endif
};

View file

@ -0,0 +1,68 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include "task.h"
#include "shortmessageconnection.h"
ShortMessageConnection::
ShortMessageConnection(const std::string &label, Task *owner,
const std::string &hostname, uint16_t port) :
SocketConnection(label, owner, hostname, port) {
}
ShortMessageConnection::
ShortMessageConnection(const std::string &label, Task *owner, int fd,
const char *ip, uint16_t port) :
SocketConnection(label, owner, fd, ip, port) {
}
PollState ShortMessageConnection::connected() {
return owner()->connectionReady(this);
}
PollState ShortMessageConnection::readData(char *buf, size_t len) {
if (reading_header) {
while (len && *buf >= '0' && *buf <= '9') {
(bytes_left *= 10) += (*buf++ - '0');
--len;
}
if (!len)
return PollState::READ;
if (!bytes_left || *buf != '\n') {
err_log() << "Got unexpected data";
return PollState::CLOSE;
}
reading_header = false;
++buf;
--len;
}
if (len < bytes_left) {
msg.append(std::string(buf, len));
bytes_left -= len;
return PollState::READ;
}
msg.append(std::string(buf, bytes_left));
len -= bytes_left;
buf += bytes_left;
bytes_left = 0;
if (tellOwner(msg) == PollState::CLOSE)
return PollState::CLOSE;
msg.clear();
reading_header = true;
if (len)
return readData(buf, len);
return PollState::READ;
}
void ShortMessageConnection::sendMessage(const std::string &msg) {
std::string s = std::to_string(msg.size()) + '\n';
asyncSendData(s + msg);
}

View file

@ -0,0 +1,47 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include "socketconnection.h"
/// \brief
/// Simple protocol for exchanging messages.
///
/// Use sendMessage to send a message to peer.
///
/// When a message has been received from peer, it will be passed to
/// the owner task's msgFromConnection method.
/// When the connection is ready (connected), the owner task's connectionReady
/// method will be called.
/// The connectionReady and msgFromConnection methods must return
/// PollState::READ to keep the connection, or PollState::CLOSE to close it.
class ShortMessageConnection : public SocketConnection {
public:
/// For client sockets, connecting to a server.
ShortMessageConnection(const std::string &label, Task *owner,
const std::string &hostname, uint16_t port);
/// For already connected sockets, i.e. in a server.
ShortMessageConnection(const std::string &label, Task *owner, int fd,
const char *ip = "unknown", uint16_t port = 0);
/// Will notify owner task that a new connection is available.
PollState connected() override;
/// Will notify owner when a complete message has been retrieved.
PollState readData(char *buf, size_t len) override;
/// Send message to peer.
void sendMessage(const std::string &msg);
private:
// Message we're currently receiving, or empty.
std::string msg;
// Bytes left for the above message to be complete,
// or 0 if we're not currently receiving a message
size_t bytes_left = 0;
bool reading_header = true;
};

370
src/framework/socket.cpp Normal file
View file

@ -0,0 +1,370 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#ifdef _WIN32
#include <ws2tcpip.h>
#include <time.h>
#define NOMINMAX
#include <windows.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <net/if.h>
#endif
#ifdef __linux
#include <netinet/tcp.h>
#endif
#include <fcntl.h>
#include <sys/types.h>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <cctype>
#include <cerrno>
#include <string>
#include <iomanip>
#include <sstream>
#include <map>
#include "socket.h"
#include "task.h"
Socket::Socket(const std::string &label, Task *owner,
const std::string &hostname, uint16_t port) :
Logger(label),
_owner(owner),
_hostname(hostname),
_port(port),
_state(PollState::NONE)
{
#ifndef _WIN32
if (!port && hostname == "UnixDomain") {
int pair_sd[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair_sd) < 0) {
errno_log() << "cannot create socket pair";
_socket = 0;
} else {
_socket = pair_sd[0];
unix_domain_peer = pair_sd[1];
fcntl(pair_sd[0], F_SETFL, O_NONBLOCK|O_CLOEXEC);
fcntl(pair_sd[1], F_SETFL, O_NONBLOCK);
}
return;
}
#endif
_socket = -1;
_peer_label = _hostname + ":" + std::to_string(_port);
}
// TODO: take initial state as a parameter, default PollState::READ.
Socket::Socket(const std::string &label, Task *owner, int fd) :
Logger(label),
_owner(owner),
_socket(fd),
_hostname(""),
_port(0),
_state(PollState::READ)
{
}
Socket::~Socket() {
if (_socket >= 0) {
closeSocket(_socket);
log() << "closed socket " << _socket;
}
}
namespace {
#ifdef USE_THREADS
thread_local
#endif
std::map<std::string, struct addrinfo *> dns_cache;
}
void Socket::clearCache() {
for (auto p : dns_cache)
freeaddrinfo(p.second);
dns_cache.clear();
}
struct addrinfo *Socket::getAddressInfo(uint16_t iptype) {
auto it = dns_cache.find(_peer_label);
if (it == dns_cache.end()) {
struct addrinfo hints, *addressInfo;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_ADDRCONFIG;
const char *hostaddr;
if (_hostname.empty()) {
hints.ai_family = AF_INET6;
hints.ai_flags |= AI_PASSIVE;
log() << "wildcard address *:" << _port;
hostaddr = nullptr;
} else if (_hostname.find_first_not_of("1234567890.:") ==
std::string::npos) {
hints.ai_flags |= AI_NUMERICHOST;
log() << "numeric address " << _hostname;
hostaddr = _hostname.c_str();
} else {
log() << "dns lookup " << _hostname;
hostaddr = _hostname.c_str();
}
int res = getaddrinfo(hostaddr, std::to_string(_port).c_str(),
&hints, &addressInfo);
if (res != 0) {
err_log() << "lookup failed: " << gai_strerror(res);
return nullptr;
} else if (!addressInfo) {
err_log() << "no valid address found";
return nullptr;
}
if (!(hints.ai_flags & AI_NUMERICHOST)) {
char ip[INET6_ADDRSTRLEN];
struct sockaddr *addr = addressInfo->ai_addr;
if (addressInfo->ai_family == AF_INET) {
struct sockaddr_in *s = reinterpret_cast<sockaddr_in *>(addr);
inet_ntop(AF_INET, &s->sin_addr, ip, sizeof ip);
} else {
struct sockaddr_in6 *s = reinterpret_cast<sockaddr_in6 *>(addr);
inet_ntop(AF_INET6, &s->sin6_addr, ip, sizeof ip);
}
log() << "lookup done: " << ip;
}
auto p2 = dns_cache.insert(std::make_pair(_peer_label, addressInfo));
it = p2.first;
}
if (iptype) {
int fam = (iptype == 6) ? AF_INET6 : AF_INET;
struct addrinfo *ai = it->second;
while (ai) {
if (ai->ai_family == fam)
return ai;
ai = ai->ai_next;
}
}
return it->second;
}
void Socket::createNonBlockingSocket(struct addrinfo *addressEntry,
struct addrinfo *localAddr) {
if (_socket >= 0) {
err_log() << "socket already exists";
return;
}
int fd = ::socket(addressEntry->ai_family, addressEntry->ai_socktype,
addressEntry->ai_protocol);
if (fd == -1) {
errno_log() << "cannot create socket";
return;
}
if (!setNonBlocking(fd)) {
closeSocket(fd);
return;
}
if (localAddr && bind(fd, localAddr->ai_addr, localAddr->ai_addrlen) != 0) {
errno_log() << "cannot bind to local address";
return;
}
int res = connect(fd, addressEntry->ai_addr, addressEntry->ai_addrlen);
if (res == -1 && !isTempError()) {
errno_log() << "connect error";
closeSocket(fd);
return;
}
// All good, let's keep the socket:
_socket = fd;
_state = PollState::CONNECTING;
}
int Socket::closeSocket(int fd) {
#ifdef _WIN32
return closesocket(fd);
#else
return close(fd);
#endif
}
bool Socket::socketInError(int fd) {
int res;
socklen_t res_len = sizeof(res);
#ifdef _WIN32
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&res, &res_len) < 0)
#else
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &res_len) < 0)
#endif
return true;
if (!res)
return false;
errno = res;
return !isTempError();
}
bool Socket::setNonBlocking(int fd) {
#ifdef __APPLE__
// SO_NOSIGPIPE only for OS X
int value = 1;
int status = setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE,
&value, sizeof(value));
if (status != 0) {
errno_log() << "cannot set SO_NOSIGPIPE";
}
#endif
#ifdef _WIN32
u_long enabledParameter = 1;
int nonBlockingResult = ioctlsocket(fd, FIONBIO, &enabledParameter);
if (nonBlockingResult == -1) {
errno_log() << "cannot set socket non-blocking";
closesocket(fd);
return false;
}
#else
int nonBlockingResult = fcntl(fd, F_SETFL, O_NONBLOCK);
if (nonBlockingResult == -1) {
errno_log() << "cannot set socket non-blocking";
close(fd);
return false;
}
#endif
#ifdef __linux
int flag = 1;
int result = setsockopt(fd, IPPROTO_TCP, TCP_NODELAY,
reinterpret_cast<char *>(&flag), sizeof(int));
if (result < 0)
errno_log() << "cannot set TCP_NODELAY";
#endif
return true;
}
const char *Socket::getIp(struct sockaddr *address, uint16_t *port) {
#ifdef USE_THREADS
thread_local
#endif
static char client_ip[INET6_ADDRSTRLEN];
if (address->sa_family == AF_INET) {
struct sockaddr_in *s = reinterpret_cast<sockaddr_in *>(address);
inet_ntop(AF_INET, &s->sin_addr, client_ip, INET6_ADDRSTRLEN);
if (port)
*port = ntohs(s->sin_port);
} else {
struct sockaddr_in6 *s = reinterpret_cast<sockaddr_in6 *>(address);
inet_ntop(AF_INET6, &s->sin6_addr, client_ip, INET6_ADDRSTRLEN);
if (port)
*port = ntohs(s->sin6_port);
}
if (strncmp(client_ip, "::ffff:", 7) == 0)
return client_ip+7;
else
return client_ip;
}
const char *Socket::getIp(struct addrinfo *address, uint16_t *port) {
return getIp(address->ai_addr, port);
}
const char *Socket::getIp(int fd, uint16_t *port, bool peer) {
#ifdef USE_THREADS
thread_local
#endif
static char client_ip[INET6_ADDRSTRLEN];
static const char *no_ip = "unknown IP";
struct sockaddr_storage address;
memset(&address, 0, sizeof address);
socklen_t addrlen = sizeof(address);
int ret = peer ?
getpeername(fd, reinterpret_cast<sockaddr *>(&address), &addrlen) :
getsockname(fd, reinterpret_cast<sockaddr *>(&address), &addrlen);
if (ret < 0) {
return no_ip;
} else {
if (address.ss_family == AF_INET) {
struct sockaddr_in *s = reinterpret_cast<sockaddr_in *>(&address);
inet_ntop(AF_INET, &s->sin_addr, client_ip, INET6_ADDRSTRLEN);
if (port)
*port = ntohs(s->sin_port);
} else {
struct sockaddr_in6 *s = reinterpret_cast<sockaddr_in6 *>(&address);
inet_ntop(AF_INET6, &s->sin6_addr, client_ip, INET6_ADDRSTRLEN);
if (port)
*port = ntohs(s->sin6_port);
}
if (strncmp(client_ip, "::ffff:", 7) == 0)
return client_ip+7;
else
return client_ip;
}
}
bool Socket::createServerSocket() {
std::string ip = _hostname;
if (_socket >= 0)
return false; // Already in use!!
log() << "Listen on " << port() << " ip " << ip;
struct addrinfo *addr = getAddressInfo();
if (!addr)
return false;
int fd = ::socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
if (fd < 0) {
errno_log() << "cannot create listen socket";
return false;
}
#ifndef _WIN32
int reuse = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse,
sizeof(reuse)) < 0) {
errno_log() << "cannot reuse listen socket";
return false;
}
#endif
if (bind(fd, addr->ai_addr, addr->ai_addrlen) != 0) {
errno_log() << "cannot bind listen socket";
return false;
}
if (listen(fd, 20) != 0) {
errno_log() << "cannot listen";
return false;
}
// Socket will be -1, and state will be UNDEFINED, unless we get here:
_socket = fd;
_state = PollState::READ;
// Check port number
struct sockaddr_in6 address;
socklen_t len = sizeof(address);
if (getsockname(fd, reinterpret_cast<sockaddr *>(&address), &len) == -1)
errno_log() << "getsockname failed";
else {
_port = ntohs(address.sin6_port);
log() << "server socket " << fd << " listening on port " << _port;
}
return true;
}

237
src/framework/socket.h Normal file
View file

@ -0,0 +1,237 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#ifdef _WIN32
#include <winsock2.h>
#endif
#include "pollstate.h"
#include "logger.h"
class Task;
/// \brief
/// This is a slave to the Engine class. You can't use it directly,
/// only through its subclasses, SocketConnection or ServerSocket.
class Socket : public Logger {
friend class Engine;
public:
Socket(const std::string &label, Task *owner,
const std::string &hostname, uint16_t port);
Socket(const std::string &label, Task *owner, int fd);
/// Return task owning the socket.
Task *owner() const {
return _owner;
}
/// Return name of the host to which the socket is supposed to connect.
std::string hostname() const {
return _hostname;
}
/// Return port number to which the socket is supposed to connect.
uint16_t port() const {
return _port;
}
/// \brief
/// Return current socket state.
PollState state() const {
return _state;
}
#ifndef _WIN32
/// \brief
/// Return the peer socket descriptor.
///
/// If this is a Unix Domain socket, return the peer socket descriptor.
/// If not, return 0.
///
/// May be used in another thread or process.
int getUnixDomainPeer() const {
return unix_domain_peer;
}
#endif
virtual ~Socket();
/// \brief
/// Return the socket's cache group, or an empty string.
///
/// By default, if we have a keepalive (cached) connection to the same
/// host and port, it will be used instead of creating a
/// new connection. Override this method to disable keepalive
/// (returning empty string) or to use only a special type of
/// cached connection (returning a label for that special type).
virtual std::string cacheLabel() {
return _hostname + std::to_string(_port);
}
/// \brief
/// Return unique connection ID if connected.
///
/// Return a positive number that is unique to this connection if it is
/// active, otherwise -1.
int id() const {
return _socket;
}
/// Set the given task as owner of the socket.
virtual void setOwner(Task *t) {
_owner = t;
}
/// \brief
/// Set a time to live for the socket.
///
/// Call this to have the socket removed automatically before a given number
/// of seconds. Note: the network engine might remove the socket 1-2 seconds
/// before the timeout, so adjust the timeout value accordingly!
void setExpiry(double s) {
expiry = timeAfter(s);
}
/// Return true if the given TimePoint is after the socket's expiry.
bool hasExpired(const TimePoint &when) const {
return (expiry < when);
}
/// Return local IP address in static buffer.
const char *localIp() const {
const char *ip = getIp(socket(), nullptr, false);
return ip;
}
/// \brief
/// Return IP address of connected socket in static buffer.
///
/// Return the local IP address if peer==false, otherwise the peer IP.
static const char *getIp(int fd, uint16_t *port = nullptr,
bool peer = true);
/// Return IP address in static buffer.
static const char *getIp(struct sockaddr *address, uint16_t *port=nullptr);
/// Return IP address in static buffer.
static const char *getIp(struct addrinfo *address, uint16_t *port=nullptr);
/// Perform DNS lookup of remote host.
struct addrinfo *getAddressInfo(uint16_t iptype = 0);
protected:
/// Return true unless last syscall encountered a fatal error.
static bool isTempError() {
#ifdef _WIN32
if (WSAGetLastError() == WSAEWOULDBLOCK ||
WSAGetLastError() == WSAEINPROGRESS ||
WSAGetLastError() == WSAENOTCONN ||
!WSAGetLastError())
#else
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINPROGRESS
|| errno == EINTR || !errno)
#endif
return true;
else
return false;
}
/// Return true if socket is watched for writeability.
virtual bool wantToSend() {
return false;
}
/// \brief
/// This will be called regularly on READ_BLOCKED sockets to check if the
/// block can be lifted.
///
/// If your subclass ever returns READ_BLOCKED,
/// it should override this method to return the new state when the block
/// should be removed.
virtual PollState checkReadBlock() {
return PollState::READ_BLOCKED;
}
/// \brief
/// Notify intention of sending large amounts of data.
///
/// Normally, this is done simply by returning PollState::READ_WRITE
/// from a scoket callback.
/// This method is useful if you're not inside such a callback when you
/// find out you need to send (large amounts of) data.
void setWantToSend() {
if (_state != PollState::CLOSE)
_state = PollState::READ_WRITE;
}
/// Return file descriptor.
int socket() const {
return _socket;
}
/// Close a file descriptor.
static int closeSocket(int fd);
/// Tell the network engine that the connection should be closed.
void closeMe() {
_state = PollState::CLOSE;
}
/// \brief
/// Create socket and initiate the connection.
///
/// Will do no nothing if socket has already been created.
/// On error, socket() will return -1.
void createNonBlockingSocket(struct addrinfo *addressEntry,
struct addrinfo *localAddr=nullptr);
/// Set socket as non-blocking.
bool setNonBlocking(int fd);
/// Return true if the file descriptor has encountered a fatal error.
static bool socketInError(int fd);
/// Return true if the socket has encountered a fatal error.
bool inError() const {
if (!socketInError(_socket))
return false;
errno_log() << "failed socket " << _socket;
return true;
}
private:
void setState(PollState state) {
if (_state != PollState::CLOSE)
_state = state;
}
// Return false on failure:
bool createServerSocket();
static void clearCache();
void killMe() {
_state = PollState::KILL;
}
// Internal identifier used as key in dns_cache:
std::string _peer_label;
void setSocket(int fd) {
_socket = fd;
}
Socket(const Socket &);
Task *_owner;
int _socket;
#ifndef _WIN32
// If this is a Unix Domain socket, the peer socket descriptor will be
// stored here:
int unix_domain_peer = 0;
#endif
std::string _hostname;
uint16_t _port;
PollState _state;
TimePoint expiry = timeMax();
};

View file

@ -0,0 +1,327 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#ifdef _WIN32
#define NOMINMAX
#include <WS2tcpip.h>
#include <Windows.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
typedef long ssize_t;
#else
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#endif
#include "socketconnection.h"
#include "task.h"
#include <sys/types.h>
#ifdef USE_GNUTLS
#include <gnutls/x509.h>
#endif
SocketConnection::
SocketConnection(const std::string &label, Task *owner,
const std::string &hostname, uint16_t port,
uint16_t iptype, struct addrinfo *local_addr) :
Socket(label, owner, hostname, port) {
peer_ip = hostname;
peer_port = port;
local_ip = local_addr;
if (local_ip)
prefer_ip_type = (local_ip->ai_family == AF_INET6 ? 6 : 4);
else
prefer_ip_type = iptype;
}
#ifdef USE_THREADS
thread_local
#endif
uint64_t SocketConnection::tot_bytes_sent = 0;
#ifdef USE_THREADS
thread_local
#endif
uint64_t SocketConnection::tot_bytes_received = 0;
SocketConnection::SocketConnection(const std::string &label, Task *owner,
int fd, const char *ip, uint16_t port) :
Socket(label, owner, fd) {
peer_ip = ip;
peer_port = port;
}
void SocketConnection::closedByPeer() {
log() << "connection closed by peer";
}
PollState SocketConnection::doRead(int fd) {
ssize_t n;
#ifdef USE_GNUTLS
if (is_tls()) {
n = gnutls_record_recv(session, socket_buffer, sizeof socket_buffer);
if (n < 0) {
if (gnutls_error_is_fatal(static_cast<int>(n))) {
// Probably client terminated the connection abruptly
dbg_log() << "TLS error on socket " << fd;
return PollState::CLOSE;
}
//warn_log() << "TLS recv interrupt on socket " << fd;
return state();
}
} else
#endif
n = recv(fd, socket_buffer, sizeof socket_buffer, 0);
if (debugging)
log() << "socket " << socket() << " doRead " << n;
if (n == 0) {
log() << "socket closed by peer " << fd;
return PollState::CLOSE;
} else if (n < 0) {
if (isTempError()) {
warn_log() << "recv interrupt on socket " << fd;
return state();
} else {
errno_log() << "recv error on socket " << fd;
return PollState::CLOSE;
}
}
if (debugging && n < 1000)
log() << "socket " << socket() << "doRead <"
<< std::string(socket_buffer, static_cast<size_t>(n));
#ifdef USE_GNUTLS
// For SSL sockets, it's the tls_pull method that counts the actual number
// of bytes fetched from the network
if (!is_tls())
#endif
{
tot_bytes_received += static_cast<uint64_t>(n);
owner()->notifyBytesReceived(static_cast<uint64_t>(n));
}
// Now let the subclass take care of what we read:
if (state() == PollState::READ || state() == PollState::READ_WRITE) {
PollState ret = readData(socket_buffer, static_cast<size_t>(n));
// If there's more to read, do it immediately instead of after
// next select call:
if (ret == PollState::READ &&
static_cast<size_t>(n) == sizeof socket_buffer)
return doRead(fd);
return ret;
}
return unexpectedData(socket_buffer, static_cast<size_t>(n));
}
// Must not be called more than once on the same object!
bool SocketConnection::asyncConnect() {
if (socket() >= 0) {
log() << "internal error, connect called twice on socket" << socket();
return false;
}
struct addrinfo *addr = getAddressInfo(prefer_ip_type);
if (!addr)
return false;
createNonBlockingSocket(addr, local_ip);
if (socket() < 0)
return false;
return true;
}
PollState SocketConnection::unexpectedData(char *buf, size_t len) {
err_log() << "unexpected data arrived; will close connection: "
<< std::string(buf, std::min(len, static_cast<size_t>(30)));
return PollState::CLOSE;
}
size_t SocketConnection::sendData(const char *buf, size_t len) {
ssize_t n;
#ifdef USE_GNUTLS
if (is_tls()) {
n = gnutls_record_send(session, buf, len);
if (n < 0) {
if (gnutls_error_is_fatal(static_cast<int>(n))) {
errno_log() << "TLS write error";
closeMe();
} else {
if (!tls_send_pending) {
warn_log() << "Socket " << socket()
<< " TLS write failure: "
<< gnutls_strerror(static_cast<int>(n));
tls_send_pending = true;
}
}
return 0;
}
// To reenable the GNUTLS_E_AGAIN warning, do this:
// tls_send_pending = false;
if (debugging && n < 1000)
log() << "socket " << socket() << "sendData <"
<< std::string(buf, static_cast<size_t>(n)) << ">";
// For SSL sockets, it's the tls_push method that counts the actual
// number of bytes sent over the network
return static_cast<size_t>(n);
}
#endif
#ifdef __APPLE__
n = send(socket(), buf, len, 0);
#elif defined(_WIN32)
n = send(socket(), buf, len, 0);
#else
n = send(socket(), buf, len, MSG_NOSIGNAL);
#endif
if (debugging)
log() << "socket " << socket() << "sendData " << n << " of " << len;
if (n < 0) {
if (!isTempError()) {
errno_log() << "cannot write";
closeMe();
}
return 0;
}
if (debugging && n < 1000)
log() << "socket " << socket() << "sendData <"
<< std::string(buf, static_cast<size_t>(n)) << ">";
// Global count
tot_bytes_sent += static_cast<uint64_t>(n);
// Per task count
owner()->notifyBytesSent(static_cast<uint64_t>(n));
return static_cast<size_t>(n);
}
#ifdef USE_WEBROOT
size_t SocketConnection::sendFileData(int fd, size_t len) {
static char buf[50000];
if (len > sizeof buf)
len = sizeof buf;
ssize_t n = read(fd, buf, len);
log() << "Read " << n << " bytes from file";
if (n <= 0) {
errno_log() << "cannot read from file";
closeMe();
return 0;
}
return sendData(buf, static_cast<size_t>(n));
}
#endif
void SocketConnection::asyncSendData(const char *buf, size_t len) {
if (to_send.empty()) {
size_t sent = sendData(buf, len);
if (sent == len)
return;
to_send = std::string(buf+sent, len-sent);
} else {
to_send.append(buf, len);
}
}
PollState SocketConnection::tellOwner(const std::string &msg) {
return owner()->msgFromConnection(this, msg);
}
#ifdef USE_GNUTLS
bool SocketConnection::
init_tls_client(gnutls_certificate_credentials_t &x509_cred, bool verify_cert) {
dbg_log() << "Enable TLS on socket " << socket();
if (gnutls_init(&session, GNUTLS_CLIENT | GNUTLS_NONBLOCK) < 0)
return false;
setSessionInitialized();
if (gnutls_set_default_priority(session) < 0)
return false;
if (gnutls_server_name_set(session, GNUTLS_NAME_DNS, peer_ip.c_str(),
peer_ip.size()) < 0)
return false;
if (gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE,
x509_cred) < 0)
return false;
if (verify_cert) {
log() << "verify cert";
gnutls_session_set_verify_cert(session, peer_ip.c_str(), 0);
}
gnutls_certificate_set_verify_flags (x509_cred,
GNUTLS_VERIFY_ALLOW_BROKEN);
gnutls_handshake_set_timeout(session,
GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
// gnutls_transport_set_int(session, socket());
// We use custom send/recv functions in order to be able to count the
// number of bytes sent and received:
{
gnutls_transport_set_ptr(session,
static_cast<gnutls_transport_ptr_t>(this));
gnutls_transport_set_push_function(session, tls_push_static);
gnutls_transport_set_pull_function(session, tls_pull_static);
}
return true;
}
ssize_t SocketConnection::tls_pull(void *buf, size_t len) {
ssize_t n = recv(socket(), buf, len, 0);
//dbg_log() << "tls_pull " << n << " bytes of " << len;
if (n > 0) {
owner()->notifyBytesReceived(static_cast<uint64_t>(n));
tot_bytes_received += static_cast<uint64_t>(n);
}
return n;
}
ssize_t SocketConnection::tls_push(const void *buf, size_t len) {
#ifdef __APPLE__
ssize_t n = send(socket(), buf, len, 0);
#elif defined(_WIN32)
ssize_t n = send(socket(), buf, len, 0);
#else
ssize_t n = send(socket(), buf, len, MSG_NOSIGNAL);
#endif
//dbg_log() << "tls_push " << n << " bytes of " << len;
if (n > 0) {
owner()->notifyBytesSent(static_cast<uint64_t>(n));
tot_bytes_sent += static_cast<uint64_t>(n);
}
return n;
}
bool SocketConnection::
init_tls_server(gnutls_certificate_credentials_t &x509_cred,
gnutls_priority_t &priority_cache) {
dbg_log() << "Enable TLS on incoming socket " << socket();
if (gnutls_init(&session, GNUTLS_SERVER | GNUTLS_NONBLOCK) < 0) {
err_log() << "Cannot initialise TLS session";
return false;
}
setSessionInitialized();
if (gnutls_priority_set(session, priority_cache) < 0 ||
gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE,
x509_cred) < 0)
return false;
gnutls_certificate_server_set_request(session,
GNUTLS_CERT_IGNORE);
gnutls_handshake_set_timeout(session,
GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
// gnutls_transport_set_int(session, socket());
// We use custom send/recv functions in order to be able to count the
// number of bytes sent and received:
{
gnutls_transport_set_ptr(session,
static_cast<gnutls_transport_ptr_t>(this));
gnutls_transport_set_push_function(session, tls_push_static);
gnutls_transport_set_pull_function(session, tls_pull_static);
}
return true;
}
#endif

View file

@ -0,0 +1,327 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <algorithm>
#include "socket.h"
#ifdef USE_GNUTLS
#include <gnutls/gnutls.h>
#endif
/// \brief
/// This class implements low-level socket connection operations.
/// Inherit from it to implement protocols like HTTP.
///
/// A SocketConnection object represents a socket connection. Inherit from this
/// class to implement a "protocol" for the connection, i.e. when/what to
/// read/write through the connection.
///
/// Each SocketConnection is owned by a Task object. SocketConnection objects
/// will be added to the network Engine (a part of the EventLoop) through the
/// task's addConnection method.
///
/// The subclass will be notified through callback functions when
/// anything happens on the socket, i.e. if the socket has been closed, if data
/// has arrived, or if the socket is writable.
/// You define the callback functions by overloading the below virtual functions.
/// All operations will be performed asynchronously (non-blocking) except for
/// dns lookups, which are performed synchronously (blocking).
///
/// Note! You _must_ create the SocketConnection objects with new. The ownership
/// will then be passed to the network engine.
/// You are not allowed to delete a SocketConnection object.
/// We will delete it when the connection has been closed, which will be
///
/// 1) If you order us to close it by returning CLOSE, KEEPALIVE or KILL from
/// a callback, or
///
/// 2) After the closedByPeer callback, if the connection has been
/// closed by peer.
///
/// 3) After the connectionFailed callback, if the connection couldn't
/// be established in the first place.
///
/// The owner task will be notified before the object is deleted.
class SocketConnection : public Socket {
friend class Engine;
public:
/// Create a SocketConnection owned by the given Task. The network Engine
/// will connect to the given hostname/port, and notify through the below
/// method when the connection is ready.
/// If iptype is 4, prefer ipv4. If iptype is 6, prefer ipv6.
SocketConnection(const std::string &label, Task *owner,
const std::string &hostname, uint16_t port,
uint16_t iptype = 0, struct addrinfo *local_addr=nullptr);
#ifdef USE_GNUTLS
virtual ~SocketConnection() override {
if (tlsInitialized()) {
gnutls_deinit(session);
}
}
/// Notify that the connection will be encrypted (SSL).
void enableTLS() {
use_tls = true;
}
/// Return true if the connection will be encrypted (SSL).
bool is_tls() const {
return use_tls;
}
/// Store SSL session in cache.
gnutls_session_t cache_session() {
session_initialized = false;
return session;
}
/// Reuse cached SSL session.
void insert_cached_session(gnutls_session_t &old_session) {
session_initialized = true;
use_tls = true;
session = old_session;
gnutls_transport_set_ptr(session,
static_cast<gnutls_transport_ptr_t>(this));
}
#endif
/// \brief
/// Will be called when the connection is established.
///
/// Override it to start sending data when the connection is ready.
///
/// If a connection couldn't be established,
/// SocketConnection::connectionFailed will be called instead.
/// You must not return PollState::CONNECTING.
virtual PollState connected() {
return PollState::CLOSE;
}
/// Will be called if the connection couldn't be established.
virtual void connectionFailed(const std::string &err_msg) {
log() << "connection failed: " << err_msg;
}
/// Called when the socket has been closed by peer:
/// May be called in states READ, WRITE, READ_WRITE
virtual void closedByPeer();
/// \brief
/// Callback, called when data has arrived; len > 0.
///
/// May be called in states READ, READ_WRITE. Override this method to
/// handle the data. Return value should be the new state.
///
/// The buffer is owned by the implementation. However, you are allowed
/// to modify the contents of the buffer and you may also use
/// it in the asyncSendData call.
virtual PollState readData(char *, size_t ) {
return PollState::CLOSE;
}
/// \brief
/// Peer has sent data when it wasn't supposed to.
///
/// If peer sends data when we're not in state READ/READ_WRITE, this
/// function will be called. Default is for the socket to be closed.
/// If you return READ, any remaining async_send data will be discarded.
virtual PollState unexpectedData(char *buf, size_t len);
/// \brief
/// Callback, called when socket is writable.
///
/// May be called in states PollState::WRITE and PollState::READ_WRITE.
/// Override it to write data.
/// Return value should be the new state. Do not return PollState::WRITE or
/// PollState::READ_WRITE except after a write operation where
/// all data couldn't be sent immediately.
virtual PollState writeData() {
return PollState::CLOSE;
}
/// \brief
/// Try to send len bytes from the given buffer. Return the amount sent.
///
/// More accurately, this method will return the number of bytes that
/// could be copied into the socket's send buffer.
///
/// Please understand that the return value might be < len.
/// To safely get all data sent, you should use the
/// SocketConnection::asyncSendData
/// function instead. However, if you need to send large
/// amounts of data ("large" as in "no upper limit") as fast
/// as possible, this is the function to use.
size_t sendData(const char *buf, size_t len);
#ifdef USE_WEBROOT
size_t sendFileData(int fd, size_t len);
#endif
/// \brief
/// Send data to peer as soon as possible.
///
/// Helper function which you may call only during the
/// execution of the callbacks SocketConnection::connected,
/// SocketConnection::readData, and SocketConnection::writeData.
///
/// Send len bytes from the given buffer.
/// The callback SocketConnection::closedByPeer
/// might be executed before all data was sent.
///
/// If you need to send "unlimited" amounts of data, you cant use
/// this method; instead you must use SocketConnection::sendData.
void asyncSendData(const char *buf, size_t len);
/// \brief
/// Send data to peer as soon as possible.
///
/// Helper function which you may call only during the
/// execution of the callbacks SocketConnection::connected,
/// SocketConnection::readData, and SocketConnection::writeData.
///
/// Send len bytes from the given buffer.
/// The callback SocketConnection::closedByPeer
/// might be executed before all data was sent.
///
/// If you need to send "unlimited" amounts of data, you cant use
/// this method; instead you must use SocketConnection::sendData.
void asyncSendData(const std::string data) {
asyncSendData(data.c_str(), data.size());
}
/// \brief
/// Return number of bytes left to send after calling
/// SocketConnection::asyncSendData.
size_t asyncBufferSize() const {
return to_send.size();
}
/// Number of bytes sent by current thread
static uint64_t totBytesSent() {
return tot_bytes_sent;
}
/// Number of bytes recieved by current thread
static uint64_t totBytesReceived() {
return tot_bytes_received;
}
/// \brief
/// Reset counter for SocketConnection::totBytesSent
/// and SocketConnection::totBytesReceived.
static void resetByteCounter() {
tot_bytes_sent = 0;
tot_bytes_received = 0;
}
/// Return peer's IP address.
const std::string &peerIp() const {
return peer_ip;
}
/// Return peer's port number.
uint16_t peerPort() const {
return peer_port;
}
/// Enable debug output of data sent and received.
void dbgOn(bool b = true) {
debugging = b;
}
/// Return true if socket debugging is enabled.
bool dbgIsOn() {
return debugging;
}
protected:
/// If fd is the socket descriptor of an already established connection,
/// you may let us manage the connection by calling this constructor.
/// fd will probably be a client socket connected through a ServerSocket.
SocketConnection(const std::string &label, Task *owner, int fd,
const char *ip, uint16_t port);
/// Send a "message" to owner task. It will be executed as
/// Task::msgFromConnection(this, msg) in the owner task.
/// This is useful if you want to create SocketConnection
/// subclasseses that work with any Task.
PollState tellOwner(const std::string &msg);
private:
// Create a connection to the given host. Will be executed asynchronously,
// then one of the callbacks connected / connectionFailed will be called.
SocketConnection(Task *owner, const std::string &hostname,
unsigned int port);
SocketConnection(const SocketConnection &);
// Will return false if connection fails immediately, e.g. if DNS
// lookup fails. Otherwise callback "connected" or "connectionFailed"
// will be called - perhaps even before this call returns:
bool asyncConnect();
PollState doRead(int fd);
// In states READ and READ_BLOCKED, this will be called to check if
// we the connection should be checked for writability too.
// If you override this, make sure to return true if asyncBufferSize() > 0.
bool wantToSend() override {
return !to_send.empty();
}
PollState doWrite() {
if (to_send.empty())
return this->writeData();
if (size_t written = sendData(to_send.c_str(), to_send.size()))
to_send.erase(0, written);
return state();
}
std::string to_send;
char socket_buffer[100000];
std::string peer_ip;
struct addrinfo *local_ip;
uint16_t peer_port;
uint16_t prefer_ip_type;
#ifdef USE_THREADS
thread_local
#endif
// Per thread byte counters.
static uint64_t tot_bytes_sent, tot_bytes_received;
bool debugging = false;
#ifdef USE_GNUTLS
bool use_tls = false;
bool session_initialized = false;
bool tls_send_pending = false;
void setSessionInitialized() {
session_initialized = true;
}
bool tlsInitialized() {
return session_initialized;
}
bool init_tls_server(gnutls_certificate_credentials_t &x509_cred,
gnutls_priority_t &priority_cache);
bool init_tls_client(gnutls_certificate_credentials_t &x509_cred,
bool verify_cert);
int try_tls_handshake() {
dbg_log() << "TLS handshake socket " << socket();
return gnutls_handshake(session);
}
static ssize_t tls_pull_static(gnutls_transport_ptr_t self,
void *buf, size_t len) {
return static_cast<SocketConnection *>(self)->tls_pull(buf, len);
}
static ssize_t tls_push_static(gnutls_transport_ptr_t self,
const void *buf, size_t len) {
return static_cast<SocketConnection *>(self)->tls_push(buf, len);
}
ssize_t tls_pull(void *buf, size_t len);
ssize_t tls_push(const void *buf, size_t len);
gnutls_session_t session;
#endif
};

View file

@ -0,0 +1,103 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#ifndef _WIN32
#include "socketreceiver.h"
SocketReceiver::SocketReceiver(Task *task, int sock, pid_t peer_pid) :
ServerSocket(sock, "SocketReceiver", task),
peer(peer_pid) {
empty_data.iov_base = nullptr;
empty_data.iov_len = 0;
memset(&fdpass_msg, 0, sizeof(fdpass_msg));
fdpass_msg.msg_iov = &empty_data;
fdpass_msg.msg_iovlen = 1;
fdpass_msg.msg_control = cmsgbuf;
fdpass_msg.msg_controllen = sizeof(cmsgbuf);
cmsg = CMSG_FIRSTHDR(&fdpass_msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
fdpass_msg.msg_controllen = cmsg->cmsg_len;
memset(&parent_msg, 0, sizeof(parent_msg));
parent_msg.msg_iov = &msg_data;
parent_msg.msg_iovlen = 1;
}
SocketConnection *SocketReceiver::incoming() {
//log() << "SocketReceiver::new_client_socket()";
struct msghdr child_msg;
memset(&child_msg, 0, sizeof(child_msg));
child_msg.msg_control = cmsgbuf;
child_msg.msg_controllen = sizeof(cmsgbuf);
#ifdef USE_THREADS
thread_local
#endif
static char buf[10000];
#ifdef USE_THREADS
thread_local
#endif
static struct iovec data = { buf, sizeof(buf) };
child_msg.msg_iov = &data;
child_msg.msg_iovlen = 1;
ssize_t len = recvmsg(socket(), &child_msg, MSG_DONTWAIT);
if (len < 0) {
log() << "recvmsg() failed: " << strerror(errno);
return nullptr;
}
cmsg = CMSG_FIRSTHDR(&child_msg);
if (cmsg == nullptr || cmsg->cmsg_type != SCM_RIGHTS) {
//log() << "Error: not a file descriptor";
if (child_msg.msg_iovlen && owner()) {
struct iovec data1 = child_msg.msg_iov[0];
auto addr = reinterpret_cast<const char *>(data1.iov_base);
owner()->workerMessage(this, addr, static_cast<size_t>(len));
}
return nullptr;
}
int newfd;
memcpy(&newfd, CMSG_DATA(cmsg), sizeof(newfd));
uint16_t port;
const char *ip = Socket::getIp(newfd, &port);
dbg_log() << "Received socket " << newfd << " from " << ip
<< " port " << port;
SocketConnection *conn = owner()->newClient(newfd, ip, port, this);
if (!conn)
closeSocket(newfd);
return conn;
}
int SocketReceiver::passSocketToPeer(int fd) {
memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
log() << "passing fd " << fd << " to peer";
int ret;
if (sendmsg(socket(), &fdpass_msg, MSG_DONTWAIT) < 0)
ret = errno;
else
ret = 0;
return ret;
}
ssize_t SocketReceiver::passMessageToPeer(const char *buf, size_t len) {
msg_data.iov_base = const_cast<char *>(buf);
msg_data.iov_len = len;
ssize_t sent = sendmsg(socket(), &parent_msg, 0);
if (sent != static_cast<ssize_t>(len))
log() << "only sent " << sent << " of total " << len;
return sent;
}
#endif

View file

@ -0,0 +1,59 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <sys/socket.h>
#include "serversocket.h"
class SocketConnection;
/// \brief
/// Pass sockets and messages between processes.
///
/// Typical use is to have a master process listen on a port,
/// passing any new connections to child processes.
///
/// Both sockets and messages (data) may be passsed between processes.
///
/// A pair of Unix domain sockets must be created in the parent process,
/// to be used by one SocketReceiver in the parent and one SocketReceiver
/// in the child process.
class SocketReceiver : public ServerSocket {
public:
/// Add a SocketReceiver to the given Task.
/// The `sock` parameter shall be one of a pair of Unix domain sockets.
/// The `peer_pid` parameter is used only for logging.
SocketReceiver(Task *task, int sock, pid_t peer_pid);
/// Return connection object if new client available, else return nullptr.
virtual SocketConnection *incoming() override;
/// Return 0 for success, errno on failure
int passSocketToPeer(int fd);
/// Send data to peer. Return amount sent, or < 0 for error.
ssize_t passMessageToPeer(const char *buf, size_t len);
/// Return PID of the peer process.
pid_t peerPid() const {
return peer;
}
/// Stop communication with peer process and terminate as soon as possible.
void peerDead() {
peer = 0;
closeMe();
}
private:
pid_t peer;
// Used when passing file descriptors to peer:
struct msghdr fdpass_msg;
struct iovec empty_data;
char cmsgbuf[CMSG_SPACE(sizeof(int))];
struct cmsghdr *cmsg;
// Used when sending data to peer:
struct msghdr parent_msg;
struct iovec msg_data;
};

View file

@ -0,0 +1,27 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <queue>
#include "synchronousbridge.h"
void SynchronousClient::initialMsgToAgent(std::deque<std::string> &) {
// Override this to push messages onto the queue.
}
double SynchronousBridge::start() {
dbg_log() << "starting";
BridgeTask::start();
the_client->initialMsgToAgent(incoming_messages);
clear_queue();
return 0;
}
SynchronousBridge::~SynchronousBridge() {
}
void SynchronousBridge::sendMsgToClient(const std::string &msg) {
// The client executes code only from within the below call.
the_client->newEventFromAgent(incoming_messages, msg);
clear_queue();
}

View file

@ -0,0 +1,69 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <deque>
#include <map>
#include "../framework/bridgetask.h"
/// \brief
/// Client that only exists (or, rather, executes code) from within the bridge.
///
/// This way, it's trivial to create a non-interactive interface to the agent.
///
/// Shall be used with a SynchronousBridge.
class SynchronousClient {
public:
/// \brief
/// Send initial messages to the agent.
///
/// Override this to push messages onto the queue.
virtual void initialMsgToAgent(std::deque<std::string> &return_msgs);
/// \brief
/// Retrieve a new message from the agent.
///
/// The client will only execute code from within its implementation
/// of this method.
///
/// The client must push any return messages onto return_msgs.
virtual void newEventFromAgent(std::deque<std::string> &return_msgs,
const std::string &msg) = 0;
virtual ~SynchronousClient() { }
};
/// \brief
/// A bridge that "owns" the client.
///
/// The client only exists (or, rather, executes code) from within the bridge.
///
/// Shall be used with a SynchronousClient.
class SynchronousBridge : public BridgeTask {
public:
SynchronousBridge(Task *agent, SynchronousClient *client) :
BridgeTask(agent),
the_client(client) {
}
/// See Task::start.
double start() override;
/// Will call the client's SynchronousClient::newEventFromAgent method.
void sendMsgToClient(const std::string &msg) override;
virtual ~SynchronousBridge() override;
private:
void clear_queue() {
while (!incoming_messages.empty()) {
std::string msg = incoming_messages.front();
incoming_messages.pop_front();
log() << "sendMsgToAgent " << msg;
sendMsgToAgent(msg);
}
}
SynchronousClient *the_client;
std::deque<std::string> incoming_messages;
};

146
src/framework/task.cpp Normal file
View file

@ -0,0 +1,146 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include "task.h"
#include "socketconnection.h"
#include "serversocket.h"
#ifdef USE_THREADS
thread_local
#endif
EventLoop *Task::supervisor = nullptr;
Task::Task(const std::string &task_name) :
Logger(task_name) {
log() << "Task " << task_name << " created";
}
void Task::setResult(const std::string &res) {
if (is_finished) {
dbg_log() << "result already set, ignoring " << res;
} else {
dbg_log() << "setResult " << res;
is_finished = true;
the_result = res;
if (has_started)
supervisor->notifyTaskFinished(this);
}
}
void Task::setMessage(const std::string &msg) {
the_message = msg;
supervisor->notifyTaskMessage(this);
}
PollState Task::connectionReady(SocketConnection * /* conn */) {
log() << "connectionReady not implemented";
return PollState::CLOSE;
}
PollState Task::msgFromConnection(SocketConnection * /* conn */,
const std::string & /* msg */) {
log() << "msgFromConnection not implemented";
return PollState::CLOSE;
}
bool Task::addConnection(SocketConnection *conn) {
if (terminated()) {
if (conn)
delete conn;
return false;
}
return supervisor->addConnection(conn);
}
bool Task::addConnected(SocketConnection *conn) {
if (terminated()) {
if (conn)
delete conn;
return false;
}
return supervisor->addConnected(conn);
}
bool Task::adoptConnection(Socket *conn) {
conn->setOwner(this);
return true;
}
std::set<Socket *> Task::getMyConnections() const {
return supervisor->findConnByTask(this);
}
void Task::wakeUp() {
supervisor->wakeUpTask(this);
}
Task::~Task() {
supervisor->taskDeleted(this);
}
bool Task::addServer(ServerSocket *conn) {
if (terminated()) {
if (conn)
delete conn;
return false;
}
return supervisor->addServer(conn);
}
int Task::runProcess(const char *const argv[]) {
return supervisor->externalCommand(this, argv);
}
void Task::processFinished(int pid, int wstatus) {
log() << "Process " << pid << " finished, status " << wstatus;
}
bool Task::parseListen(const TaskConfig &tc, const std::string &log_label) {
auto to = tc.cfg().upper_bound("listen");
for (auto p=tc.cfg().lower_bound("listen"); p!=to; ++p) {
std::istringstream s(p->second);
uint16_t port;
std::string ip;
s >> port;
if (!s) {
err_log() << "Bad configuration directive: listen " << p->second;
setError("bad configuration directive");
return false;
}
s >> ip;
bool tls;
if (ip.empty()) {
tls = false;
} else if (ip == "tls") {
ip.clear();
tls = true;
} else {
std::string tmp;
s >> tmp;
tls = (tmp == "tls");
}
auto sock = new ServerSocket(log_label, this, port, ip);
#ifdef USE_GNUTLS
if (tls) {
std::string cert, key, password;
s >> cert >> key >> password;
if (key.empty() || !tlsSetKey(sock, cert, key, password)) {
err_log() << "Bad configuration: " << p->second;
setError("cannot use TLS certificate");
return false;
}
log() << "Port " << port << " enable TLS";
}
#else
if (tls) {
err_log() << "cannot enable TLS, will not listen on port " << port;
return false;
}
#endif
if (!addServer(sock)) {
setError("cannot listen");
return false;
}
}
return true;
}

519
src/framework/task.h Normal file
View file

@ -0,0 +1,519 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <set>
#include <fstream>
#include "logger.h"
#include "taskconfig.h"
#include "eventloop.h"
class Socket;
class ServerSocket;
class SocketConnection;
enum class PollState;
class SocketReceiver;
class WorkerProcess;
/// \brief
/// The purpose of a task is to manage socket connections, and/or to execute
/// timers.
///
/// This is essentially an abstract base class. You must create subclasses and
/// pass subclass objects to the EventLoop's addTask method.
///
/// Timers are very simple; the start method must return the number of seconds
/// until the timerEvent method should be called. The overridden timerEvent can
/// do whatever it needs and then return the number of seconds until it should
/// be called again. A value <= 0 means it will never be called again.
///
/// Note that the application will be single threaded. Thus, timers (and other
/// callbacks) must avoid blocking and generally try do be as quick as possible,
/// otherwise all subsequent timers will be delayed.
///
/// The Task may create SocketConnection objects and add them using the
/// addConnection method. It may also create ServerSocket objects and
/// add them with addServer; whenever a client connects to the server,
/// you will be notified through the newClient method.
class Task : public Logger {
public:
/// \brief Create a task with the given name.
///
/// The name will be used as a log label
/// and may be retrieved using the inherited Logger::label method.
///
/// The EventLoop might not be available when the constructor is run, so
/// do not use it in the constructor of any subclass. Any asynchronous
/// initialization should be performed in the Task::start() method.
Task(const std::string &task_name);
/// A Task object will be deleted by the EventLoop after it has finished
/// and its parent task has been notified.
virtual ~Task();
/// When the EventLoop starts executing a task, it will call its start
/// method. All non-trivial initialization, e.g. creating new socket
/// connections, should be performed in the start method.
///
/// If the task needs a timer, the start method must return the number of
/// seconds until timerEvent should
/// be called, or <= 0 if you don't want it to be called.
virtual double start() {
dbg_log() << "Task starting. No timer.";
return 0;
}
/// Return number of seconds until this method should be
/// called again, or <= 0 if you don't want it to be called again.
virtual double timerEvent() {
dbg_log() << "Default timerEvent: will kill task.";
setTimeout();
return 0;
}
/// Run timerEvent after s seconds instead of previous value.
void resetTimer(double s) {
supervisor->resetTimer(this, s);
}
/// \brief
/// Return true if the task has finished normally.
///
/// This method is meant to be used in the taskFinished callback.
bool finishedOK() const {
return (has_started && is_finished && !was_killed &&
!was_error && !was_timeout);
}
/// \brief
/// Return true if the task is finished and was aborted by another task.
///
/// This method is meant to be used in the taskFinished callback.
bool wasKilled() const {
return was_killed;
}
/// \brief
/// Return true if the task terminated with an error.
///
/// This method is meant to be used in the taskFinished callback.
bool wasError() const {
return was_error;
}
/// \brief
/// Return true if the task terminated with a timeout.
///
/// This method is meant to be used in the taskFinished callback.
bool wasTimeout() const {
return was_timeout;
}
/// Return true if the task has been added to the EventLoop and its
/// Task::start() method has been executed.
bool hasStarted() const {
return has_started;
}
/// Call this in the start() callback if you want all child tasks to be
/// killed when this task is finished.
void killChildTaskWhenFinished() {
kill_children = true;
}
/// Ignore this unless the task is a server task.
/// If a new remote connection is made through any ServerSocket object
/// owned by us, the client socket, ip address and port number will
/// be passed to the below method. Override it to create and return
/// an object of a subclass to SocketConnection, otherwise the client
/// socket will be closed. The object will be owned by the implementation
/// and will be deleted when the connection has been closed. You _must_
/// create the object with new.
virtual SocketConnection *newClient(int, const char *, uint16_t,
ServerSocket *) {
return nullptr;
}
/// Request for me to adopt a socket owned by some other task.
/// Return false to reject. Otherwise set me as owner and return true.
virtual bool adoptConnection(Socket *conn);
/// This will be called to notify us when a new client socket object
/// has been successfully added to this task.
/// If you need to know that, override this method.
virtual void connAdded(SocketConnection *) {
}
/// This will be called when a client socket object has been removed from
/// this task, just before it is deleted.
/// If you need to know that, override this method.
virtual void connRemoved(SocketConnection *) {
}
/// This will be called to notify us when a new server (listening) socket
/// object has been successfully added to this task.
/// If you need to know that, override this method.
virtual void serverAdded(ServerSocket *) {
}
/// This will be called when a server socket object has been removed from
/// this task, just before it is deleted.
/// If you need to know that, override this method.
virtual void serverRemoved(ServerSocket *) {
}
/// To get the "result" of the task after it has finished.
std::string result() const {
return the_result;
}
/// Return all current connections.
std::set<Socket *> getMyConnections() const;
/// Return true if the connection still exists.
bool isActive(Socket *conn) const {
return supervisor->isActive(conn);
}
/// Restart all idle connections
void wakeUp();
/// If s is idle, restart it and return true. Otherwise return false.
bool wakeUpConnection(SocketConnection *s) {
return supervisor->wakeUpConnection(s);
}
/// Terminate and remove a connection.
void cancelConnection(SocketConnection *s) {
supervisor->cancelConnection(s);
}
/// Return the current (outgoing) message.
std::string message() const {
return the_message;
}
/// Notify that this task will be observing task "to". This task will be
/// notified through a call to taskFinished if "to" terminates before me.
/// (A parent task is observing its child tasks by default.)
bool startObserving(Task *to) {
return supervisor->startObserving(this, to);
}
/// Execute receiver's Task::handleExecution method immediately. The call
/// will be ignored unless this task is observing the receiver.
/// Note that you could also call `receiver->handleExecution()` (or any
/// other method in receiver) directly. However, that might be dangerous
/// since your pointer to the receiver is a _weak reference_ and you
/// must somehow make sure that the receiver still exists.
/// The advantages of using this API instead of directly calling
/// methods in the receiver task are:
/// 1. Safer; the call will be ignored if the receiver task doesn't exist.
/// 2. The sender and receiver classes do not have to know each other.
/// 3. Since you are observing the receiver, you will be notified
/// when the receiver task terminates.
///
/// *Note*:
/// Essentially, the receiver's Task::handleExecution method executes from
/// within the sender's method (event handler). If the receiver in any
/// way modifies the sender from within its Task::handleExecution method,
/// bad things may happen. Code the Task::handleExecution methods carefully.
void executeHandler(Task *receiver, const std::string &message) {
if (supervisor->isObserving(this, receiver) && !receiver->terminated())
receiver->handleExecution(this, message);
else
log() << "Will not call handleExecution since task isn't observed.";
}
/// \brief
/// Return number of seconds since the task was started.
///
/// The start time of a task is when the Task::start method is executed.
/// Do not call this method until the task has been added to the EventLoop.
double elapsed() const {
return secondsSince(start_time);
}
#ifndef _WIN32
/// Override this if you intend to start child processes with
/// createWorker(). It will be executed in the child process.
/// wno is the argument last parameter you supplied to createWorker().
/// You must create a Task with new and return its address.
virtual Task *createWorkerTask(unsigned int wno) {
log() << "missing createWorkerTask, cannot create worker " << wno;
return nullptr;
}
/// This will be called in child process directly before exit.
/// Override if you need to clean up after createWorkerTask.
virtual void finishWorkerTask(unsigned int ) {
}
/// Called during startup of worker process, once for each SocketReceiver
/// object. Will be called before serverAdded with the same SocketReceiver.
/// Overload this if your worker process has two or more SocketReceiver
/// objects with different confguration (i.e. different SSL keys).
virtual void newWorkerChannel(SocketReceiver *, unsigned int ) {
}
/// Called if parent/worker sends a message through a SocketReceiver:
virtual void workerMessage(SocketReceiver *, const char *buf, size_t len) {
log() << "Worker message: " << std::string(buf, len);
}
#endif
/// Normally, SocketConnection objects are designed for a specific type of
/// Task, i.e. a HttpServerConnection might be designed for a WebServerTask.
/// However, some SocketConnection subclasses are "generic" and meant to
/// work with any Task object as owner. When such SocketConnection objects
/// want to contact the owner, they may use this method to signal that the
/// socket has been connetced.
///
/// Return PollState::READ to keep the connection,
/// or PollState::CLOSE to close it.
virtual PollState connectionReady(SocketConnection * /* conn */);
/// Normally, SocketConnection objects are designed for a specific type of
/// Task, i.e. a HttpServerConnection might be designed for a WebServerTask.
/// However, some SocketConnection subclasses are "generic" and meant to
/// work with any Task object as owner. When such SocketConnection objects
/// have a message for their owner, they may use this method.
///
/// Return PollState::READ to keep the connection,
/// or PollState::CLOSE to close it.
virtual PollState msgFromConnection(SocketConnection * /* conn */,
const std::string & /* msg */);
/// Number of bytes sent through SocketConnection objects owned by me.
uint64_t bytesSent() const {
return tot_bytes_sent;
}
/// Number of bytes received through SocketConnection objects owned by me.
uint64_t bytesReceived() const {
return tot_bytes_received;
}
/// \brief
/// Reset the values for the methods Task::bytesSent
/// and Task::bytesReceived.
void resetByteCount() {
tot_bytes_sent = 0;
tot_bytes_received = 0;
}
/// \brief
/// Notify the task that data has been sent on its behalf.
///
/// For use *only* by custom SocketConnection subclasses after calling send
/// directly on a socket.
/// Don't call the this methods unless you know what you're doing.
void notifyBytesSent(uint64_t n) {
tot_bytes_sent += n;
}
/// \brief
/// Notify the task that data has been received on its behalf.
///
/// For use *only* by custom SocketConnection subclasses after calling recv
/// directly on a socket.
/// Don't call the this methods unless you know what you're doing.
void notifyBytesReceived(uint64_t n) {
tot_bytes_received += n;
}
protected:
/// To add a new client connection. Will delete conn and return false
/// on immediate failure. connRemoved will not be called in that case.
///
/// The connection must belong to a _running_ task (probably this one).
/// I.e. the task must have been added to the EventLoop and the start()
/// method must have been called. You can't do this in the constructor!
///
/// If successfully added, we will call connAdded and return true.
/// The EventLoop takes ownership of the SocketConnection object and
/// will call connRemoved and then delete it if the connection fails or
/// is closed or when the task is finished.
bool addConnection(SocketConnection *conn);
/// Use this if conn contains a socket that has already been connected.
/// Returns false (and deletes conn) on failure.
/// On success, returns true and calls connAdded on owner task,
/// then calls connected() on conn to get initial state.
bool addConnected(SocketConnection *conn);
/// As Task::addConnected, but with a server connection.
bool addServer(ServerSocket *conn);
/// Check config file for listening (server) sockets. The sockets
/// are added to the task, with the given log_label. E.g.
///
/// listen 80 192.36.30.2
/// listen 8080
/// listen 443 tls /etc/ssl/fd.crt /etc/ssl/fd.key 4lEGyLax
///
/// The value of the listen parameter is either a port number,
/// e.g. "8080", or a port number followed by a space and an ip address,
/// e.g. "8080 192.168.0.1". The address is either ipv4 or ipv6.
/// If connections are to be encrypted, add "tls" followed by
/// paths to your SSL certificate and private key, optionally followed
/// by the password (if the key is protected by a password).
bool parseListen(const TaskConfig &tc, const std::string &log_label);
#ifdef USE_GNUTLS
/// Use SSL certificate for a listening socket.
virtual bool tlsSetKey(ServerSocket *conn, const std::string &crt_path,
const std::string &key_path, const std::string &password) {
return supervisor->tlsSetKey(conn, crt_path, key_path, password);
}
#endif
/// When the task is done, it should notify the EventLoop by calling the
/// Task::setResult method. Then the task's parent will be notified and the
/// task will be deleted. The "result" of the task should be a non-empty
/// string on success, and an empty string on timeout or error.
/// Of course, subclasses can calculate more complex custom "results" in
/// addition to this simple string.
void setResult(const std::string &res);
/// Called to signal fatal error. May be overridden to "catch" errors.
/// It should always call Task::setResult with an empty string.
virtual void setError(const std::string &msg) {
log() << "Task failure: " << msg;
was_error = true;
setResult("");
}
/// Called to signal timeout. May be overridden to "catch" timeouts.
/// It should always call Task::setResult with an empty string.
virtual void setTimeout() {
was_timeout = true;
setResult("");
}
/// Call to signal that the task has a "message" to deliver. The parent
/// will be notified using the taskMessage method. Only the last message
/// will be stored, and it may be retrieved using the message method.
void setMessage(const std::string &msg);
/// Called when an observed task, e.g. a child task, terminates.
/// Override this method to handle such events.
virtual void taskFinished(Task *task) {
dbg_log() << "Task " << task->label() << " died, no handler defined.";
}
/// Called when an a child task has (set/sent) a message.
/// Override this method to handle such events.
virtual void taskMessage(Task *task) {
dbg_log() << "No taskMessage handler implemented for " << task->label();
}
/// Callback to execute code on behalf of another Task.
virtual void handleExecution(Task *sender, const std::string &message) {
dbg_log() << "Event " << message << " from "
<< (sender ? sender->label() : "unknown")
<< ", no handler implemented";
}
/// \brief
/// Return true if task is finished.
///
/// By default, this will return true if the task has called
/// the Task::setResult method.
///
/// The task will be deleted very soon unless return value is false. So
/// this method is mostly useful within the Task subclass itself, checking
/// if it is about to be removed.
bool terminated() const {
return is_finished;
}
/// Insert another Task for execution by the EventLoop.
void addNewTask(Task *task, Task *parent = nullptr) {
supervisor->addTask(task, parent);
}
#ifdef USE_THREADS
/// Run task in a new thread.
void addNewThread(Task *task, const std::string &name="ThreadLoop",
std::ostream *log_file = nullptr,
Task *parent = nullptr) {
supervisor->spawnThread(task, name, log_file, parent);
}
#endif
/// Add all my child tasks to the given set.
void getMyTasks(std::set<Task *> &tset) {
supervisor->getChildTasks(tset, this);
}
/// Terminate all my child tasks.
void abortMyTasks() {
supervisor->abortChildTasks(this);
}
/// Terminate a task.
void abortTask(Task *task) {
supervisor->abortTask(task);
}
/// Terminate all tasks and exit the EventLoop.
void abortAllTasks() {
supervisor->abort();
}
/// Start execution of external command, return an ID.
/// On immediate failure, return value is -1.
int runProcess(const char *const argv[]);
/// Will be called to notify when an external process has terminated.
virtual void processFinished(int pid, int wstatus);
#ifndef _WIN32
/// \brief
/// Run task returned by this->createWorkerTask in a child process.
/// Return nullptr on failure.
WorkerProcess *createWorker(std::ostream *log_file = nullptr,
unsigned int channels = 1,
unsigned int wno = 0) {
return supervisor->createWorker(this, log_file, channels, wno);
}
/// \brief
/// Run task returned by this->createWorkerTask in a child process.
/// Return nullptr on failure.
WorkerProcess *createWorker(const std::string &log_file_name,
unsigned int channels = 1,
unsigned int wno = 0) {
return supervisor->createWorker(this, log_file_name, channels, wno);
}
#endif
private:
friend class EventLoop;
void setTerminated() {
is_finished = true;
}
// Called by EventLoop when task is scheduled for execution:
double begin() {
has_started = true;
start_time = timeNow();
return this->start();
}
#ifdef USE_THREADS
thread_local
#endif
static EventLoop *supervisor;
TimePoint start_time;
std::string the_result;
std::string the_message;
uint64_t tot_bytes_sent = 0, tot_bytes_received = 0;
bool is_finished = false;
bool has_started = false;
bool was_killed = false, was_error = false, was_timeout = false;
bool kill_children = false;
bool is_child_thread = false;
};

View file

@ -0,0 +1,196 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <sstream>
#include <fstream>
#include <iostream>
#include "taskconfig.h"
#include "eventloop.h"
#include "../json11/json11.hpp"
TaskConfig::TaskConfig(std::istream &cfg_stream) {
_load(cfg_stream);
}
TaskConfig::TaskConfig(const std::string &cfg_text) {
std::istringstream cfg_stream(cfg_text);
_load(cfg_stream);
}
const char *BadTaskConfig::what() const noexcept {
return err_msg.c_str();
}
BadTaskConfig::~BadTaskConfig() noexcept {
}
TaskConfig TaskConfig::load(const std::string &filename) {
std::ifstream cfg_stream(filename);
return TaskConfig(cfg_stream);
}
std::string TaskConfig::value(const std::string &key, const std::string &default_value) const {
auto range = the_config.equal_range(key);
if (range.first == range.second)
return default_value;
--range.second;
return range.second->second;
}
void TaskConfig::openlog(std::ofstream &logger, bool append) const {
auto p = the_config.find("logfile");
if (p != the_config.end() && p->second != "-") {
logger.open(p->second, append ? std::ios::app : std::ios::trunc);
if (logger) {
Logger::setLogFile(logger);
#ifndef _WIN32
EventLoop::setLogFilename(p->second);
#endif
}
}
}
void TaskConfig::add(const std::string &key, const std::string &val) {
the_config.insert(std::make_pair(key, val));
}
void TaskConfig::addLine(const std::string &line) {
size_t pos = line.find('#');
if (pos != std::string::npos) {
addLine(line.substr(0, pos));
return;
}
if (line.empty())
return;
pos = line.find(" ");
if (pos == std::string::npos)
the_config.insert(std::make_pair(line, std::string()));
else
the_config.insert(std::make_pair(line.substr(0, pos),
line.substr(pos+1)));
}
void TaskConfig::workerAttributes(const std::set<std::string> &attrs) {
std::vector<std::string> to_add;
for (auto &p : the_config)
if (attrs.find(p.first) != attrs.end())
to_add.push_back(p.first + " " + p.second);
for (auto &line : to_add)
add("workercfg", line);
}
void TaskConfig::_load(std::istream &cfg_stream) {
std::string line;
while (getline(cfg_stream, line)) {
addLine(line);
}
if (!cfg_stream.eof())
throw BadTaskConfig();
}
std::ostream &operator<<(std::ostream &out, const TaskConfig &tc) {
out << "[ ";
for (auto &p : tc)
out << p.first << " --> " << p.second << " ";
out << ']';
return out;
}
std::set<std::string>
TaskConfig::parseList(const std::string &category) const {
std::string val;
std::set<std::string> res;
auto to = the_config.upper_bound(category);
for (auto p=the_config.lower_bound(category); p!=to; ++p) {
std::istringstream s(p->second);
while (s >> val)
res.insert(val);
}
return res;
}
void TaskConfig::parseArgs(int &argc, char **&argv) {
int apos = 0;
while (++apos < argc) {
std::string arg = argv[apos];
if (arg.substr(0, 2) != "--")
break;
arg.erase(0, 2);
if (arg.empty()) {
++apos;
break;
}
auto pos = arg.find('=');
if (pos == std::string::npos)
the_config.insert(std::make_pair(arg, std::string()));
else if (pos)
the_config.insert(std::make_pair(arg.substr(0, pos),
arg.substr(pos+1)));
// else ignore
}
if (--apos) {
argc -= apos;
argv[apos] = argv[0];
argv += apos;
}
}
std::map<std::string, std::string>
TaskConfig::parseKeyVal(const std::string &category) const {
std::string key, val;
std::map<std::string, std::string> res;
auto to = the_config.upper_bound(category);
for (auto p=the_config.lower_bound(category); p!=to; ++p) {
std::istringstream s(p->second);
if (s >> key >> val)
res.insert(std::make_pair(key, val));
}
return res;
}
/*Preconditions: New configs set with method "saveConfigurationOption"
Old config options loaded into the_config in MeasurementAgent() (loadJsonFromFile).
Postconditions: Configuration options transferred onto file */
bool TaskConfig::saveJsonToFile(const std::string &filename) {
std::fstream cfgOptionsFile;
//Create file if not existing. Write the updated config in json-format to file.
cfgOptionsFile.open(filename, std::fstream::out);
if (cfgOptionsFile.is_open()) {
cfgOptionsFile << json11::Json(the_config).dump();
} else {
Logger::log("TaskConfig") << "Unable to open config file";
}
cfgOptionsFile.close();
return bool(cfgOptionsFile);
}
/*
Used to load preexisting configuration options from file into json-object for further use.
*/
TaskConfig TaskConfig::loadJsonFromFile(const std::string &filename){
std::string fileContent;
std::ifstream inFile;
std::string err;
TaskConfig cfg;
inFile.open(filename);
if (inFile.is_open() && inFile) {
//consume entire inFile, from beginning to end.
fileContent.assign( (std::istreambuf_iterator<char>(inFile)),
(std::istreambuf_iterator<char>()) );
} else {
Logger::log("TaskConfig") << "Unable to open config file";
}
json11::Json JsonObj = json11::Json::parse(fileContent, err);
inFile.close();
for (auto p : JsonObj.object_items()) {
cfg.add(p.first, p.second.string_value());
}
return cfg;
}

161
src/framework/taskconfig.h Normal file
View file

@ -0,0 +1,161 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <map>
#include <set>
#include <string>
#include <stdexcept>
#include <iostream>
#include <fstream>
/// Exception thrown on syntax errors in task config
class BadTaskConfig : public std::exception {
public:
BadTaskConfig(const std::string &msg = "cannot read config file") :
err_msg(msg) {
}
const char *what() const noexcept override;
BadTaskConfig(const BadTaskConfig &old) : err_msg(old.err_msg) {
}
~BadTaskConfig() noexcept override;
std::string err_msg;
};
/// \brief
/// Read configuration from file or string
///
/// Empty lines are ignored.
///
/// A # character means the rest of the line is a comment.
///
/// All other lines must contain a configuration directive.
/// If the line contains a space, the directive ends at the first space,
/// and the rest of the line is the value of the directive.
///
/// Example:
///
/// logfile /var/log/my_service.log
/// name My Service # Will be printed in greeting message
///
/// listen 80
/// listen 443 tls /etc/pki/tls/certs/mycert.pem /etc/pki/tls/private/mycert.pem
class TaskConfig {
public:
/// Empty configuration.
TaskConfig() {}
/// Load configuration from file.
TaskConfig(std::istream &cfg_stream);
/// Load configuration from string.
TaskConfig(const std::string &cfg_text);
/// Add a directive to the config.
void add(const std::string &key, const std::string &val);
/// Replace value(s) of a directive with a new one.
void set(const std::string &key, const std::string &val) {
the_config.erase(key);
add(key, val);
}
/// Remove value(s) of a directive.
void erase(const std::string &key) {
the_config.erase(key);
}
/// Set value of a directive unless already set.
void setDefault(const std::string &key, const std::string &val) {
if (the_config.find(key) == the_config.end())
add(key, val);
}
/// Incrementally add to the config.
void addLine(const std::string &line);
/// Start iterator to loop over the config.
std::multimap<std::string, std::string>::iterator begin() {
return the_config.begin();
}
/// End iterator to loop over the config.
std::multimap<std::string, std::string>::iterator end() {
return the_config.end();
}
/// Start const iterator to loop over the config.
std::multimap<std::string, std::string>::const_iterator begin() const {
return the_config.begin();
}
/// End const iterator to loop over the config.
std::multimap<std::string, std::string>::const_iterator end() const {
return the_config.end();
}
/// Make a set of directives available to worker processes.
void workerAttributes(const std::set<std::string> &attrs);
/// Read config from file.
static TaskConfig load(const std::string &filename);
/// Return the parsed configuration.
const std::multimap<std::string, std::string> &cfg() const {
return the_config;
}
/// Return value of last occurence of key. Return default_value if key does not exist.
std::string value(const std::string &key, const std::string &default_value = "") const;
/// Return true if key exists, otherwise false:
bool hasKey(const std::string &key) const {
return the_config.find(key) != the_config.end();
}
/// Return a range of the key/value paris for the given key.
std::pair<std::multimap<std::string, std::string>::const_iterator,
std::multimap<std::string, std::string>::const_iterator>
range(const std::string &key) const {
return the_config.equal_range(key);
}
/// \brief
/// Log to the file specified by the `logfile` directive.
///
/// If the `logfile` key exists, and its value is not "-", try to use the
/// value as a file name for the log.
void openlog(std::ofstream &logger, bool append = false) const;
/// \brief
/// Split config value into non-blank strings.
///
/// Return set of all non-whitespace strings listed after the configuration
/// directive given by second parameter.
std::set<std::string>
parseList(const std::string &category = "whitelist") const;
/// Parse command line arguments starting with "--":
void parseArgs(int &argc, char **&argv);
/// Return map of all key-value pairs of strings listed after the
/// configuration directive given by second parameter.
std::map<std::string, std::string>
parseKeyVal(const std::string &category = "user") const;
/// Store contents as a JSON object. Return false on failure.
bool saveJsonToFile(const std::string &filename);
/// \brief Load key/value pairs from JSON object.
///
/// Values that are not strings will
/// be ignored. Return empty object on failure.
static TaskConfig loadJsonFromFile(const std::string &filename);
private:
void _load(std::istream &cfg_stream);
std::multimap<std::string, std::string> the_config;
};
std::ostream &operator<<(std::ostream &out, const TaskConfig &tc);

View file

@ -0,0 +1,42 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include "threadbridge.h"
#include <iostream>
ThreadBridge::ThreadBridge(Task *agent, double tick) :
BridgeTask(agent),
tick_len(tick) {
}
double ThreadBridge::start() {
BridgeTask::start();
return tick_len;
}
double ThreadBridge::timerEvent() {
std::string msg;
while (true) {
method_queue.fetch(msg);
if (msg.empty())
return tick_len;
sendMsgToAgent(msg);
msg.clear();
}
}
void ThreadBridge::pushToAgent(const std::string &msg) {
dbg_log() << "To agent: " << msg;
method_queue.push(msg);
}
std::string ThreadBridge::popFromAgent() {
std::string msg;
event_queue.fetch(msg);
return msg;
}
void ThreadBridge::sendMsgToClient(const std::string &msg) {
dbg_log() << "To client: " << msg;
event_queue.push(msg);
}

View file

@ -0,0 +1,37 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include "bridgetask.h"
#include "msgqueue.h"
/// \brief
/// Bridge using a thread safe queue to enable communication
/// between agent and client.
class ThreadBridge : public BridgeTask {
public:
ThreadBridge(Task *agent = nullptr, double tick=0.05);
/// API for client to send message to the agent.
void pushToAgent(const std::string &msg)
/// \brief
/// API for client to retrieve next message from agent.
///
/// If no messages are available, an empty string will be returned.
/// Client shoud call this regularly.
std::string popFromAgent();
/// Pass message to the client.
void sendMsgToClient(const std::string &msg) override;
/// Initiate timer to be called after `tick` seconds.
double start() override;
/// Push queued messages for the agent. Called every `tick` seconds.
double timerEvent() override;
private:
MsgQueue<std::string> method_queue, event_queue;
double tick_len;
};

View file

@ -0,0 +1,42 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include "unixdomainbridge.h"
#include "shortmessageconnection.h"
UnixDomainBridge::UnixDomainBridge(Task *agent) :
BridgeTask(agent),
msg_conn(new ShortMessageConnection("UDBridgeConn", this, "UnixDomain", 0)) {
}
double UnixDomainBridge::start() {
BridgeTask::start();
addConnected(msg_conn);
return 0;
}
void UnixDomainBridge::sendMsgToClient(const std::string &msg) {
msg_conn->sendMessage(msg);
}
int UnixDomainBridge::getClientSocket() const {
if (!msg_conn)
return 0;
return msg_conn->getUnixDomainPeer();
}
int UnixDomainBridge::getAgentSocket() const {
if (!msg_conn || msg_conn->id() < 0)
return 0;
return msg_conn->id();
}
PollState UnixDomainBridge::connectionReady(SocketConnection * /* conn */) {
return PollState::READ;
}
PollState UnixDomainBridge::msgFromConnection(SocketConnection *,
const std::string &msg) {
sendMsgToAgent(msg);
return PollState::READ;
}

View file

@ -0,0 +1,49 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include "bridgetask.h"
class ShortMessageConnection;
/// \brief
/// Bridge using a pair of Unix domain sockets to enable communication
/// between agent and client.
class UnixDomainBridge : public BridgeTask {
public:
/// \brief
/// Create a bridge to the given agent task.
///
/// A pair of Unix domain sockets will be used to enable communication
/// between agent and client.
UnixDomainBridge(Task *agent = nullptr);
/// Pass message to the client.
void sendMsgToClient(const std::string &msg) override;
/// \brief
/// Get client's socket descriptor.
///
/// Return 0 on failure.
///
/// *Note:* client may run in another thread or process.
/// Use in child, close in parent after fork.
int getClientSocket() const;
/// Close in child after fork.
int getAgentSocket() const;
/// See Task::connectionReady.
PollState connectionReady(SocketConnection * /* conn */) override;
/// Will be called when client has sent a message.
PollState msgFromConnection(SocketConnection * /* conn */,
const std::string &msg) override;
/// See Task::start.
double start() override;
private:
ShortMessageConnection *msg_conn;
};

View file

@ -0,0 +1,87 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include "unixdomainclient.h"
#include "bridgetask.h"
void UnixDomainClient::pushToAgent(const std::string &msg) {
to_agent += std::to_string(msg.size());
to_agent += '\n';
to_agent += msg;
auto n = send(client_socket, to_agent.c_str(), to_agent.size(), 0);
if (n > 0)
to_agent.erase(0, n);
}
std::string UnixDomainClient::pollAgent() {
ssize_t n;
while (true) {
char buffer[50000];
n = recv(client_socket, buffer, sizeof buffer, 0);
if (n <= 0)
break;
to_client.append(buffer, n);
// Maybe we should have a max size for a single message.
}
auto err = errno;
auto pos = to_client.find('\n');
if (pos != std::string::npos) {
// Check if a new (complete) message has arrived.
try {
auto msg_len = std::stoul(to_client.substr(0, pos));
if (to_client.size() > pos + msg_len) {
++pos;
std::string result = to_client.substr(pos, msg_len);
to_client.erase(0, pos+msg_len);
return result;
}
} catch (...) {
return BridgeTask::agentTerminatedMessage("bad data");
}
}
if (n == 0)
return BridgeTask::agentTerminatedMessage("lost connection");
if (err && err != EAGAIN && err != EWOULDBLOCK &&
err != EINPROGRESS && err != EINTR) {
return BridgeTask::agentTerminatedMessage("connection error");
}
return "";
}
bool UnixDomainClient::flushToAgent() {
if (to_agent.empty())
return true;
auto n = send(client_socket, to_agent.c_str(), to_agent.size(), 0);
if (n > 0) {
to_agent.erase(0, n);
return to_agent.empty();
}
return false;
}
std::string UnixDomainClient::waitForMsgFromAgent(unsigned long timeout_us) {
std::string msg = pollAgent();
if (!msg.empty())
return msg;
struct timeval timeout;
timeout.tv_sec = timeout_us/1000000;
timeout.tv_usec = timeout_us%1000000;
fd_set readFds, errFds;
FD_ZERO(&readFds);
FD_ZERO(&errFds);
FD_SET(client_socket, &readFds);
FD_SET(client_socket, &errFds);
select(client_socket + 1, &readFds, nullptr, &errFds,
timeout_us ? &timeout : nullptr);
return pollAgent();
}

View file

@ -0,0 +1,62 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <string>
/// \brief
/// Client communicating with an agent task using a UnixDomainBridge.
///
/// This class is meant to be used only _outside_ the event loop, i.e. from some
/// other thread or process. It is used together with a UnixDomainBridge, which
/// runs in the event loop. After creating the UnixDomainBridge object, call
/// getClientSocket() on it to get a file descriptor which should be used as
/// an argument to the constructor of this class. Then make sure the
/// UnixDomainBridge object runs in an event loop, but not in the same thread or
/// process as this class.
class UnixDomainClient {
public:
/// Create client using one of a pair of Unix domain sockets.
UnixDomainClient(int peer_fd) :
client_socket(peer_fd) {
}
/// \brief
/// Return a line of data from the agent.
///
/// Will return an empty string if no message is available.
///
/// Poll regularly or monitor the socket descriptor.
std::string pollAgent();
/// As UnixDomainClient::pollAgent, but block until a message is available
/// or until timeout_us
/// microseconds have passed (or forever, if timeout_us is 0).
std::string waitForMsgFromAgent(unsigned long timeout_us = 0);
/// \brief
/// Send message to agent.
///
/// *Note:* If the message is large (perhaps >200KB),
/// it's likely that the complete message can't be delivered to the agent
/// immediately. In that case, it will be buffered and you may have to
/// call pushToAgent (with new messages) or flushToAgent() repeatedly until
/// the buffer is drained. (Of course, unless the agent actively reads the
/// messages, the buffer can't be drained.)
void pushToAgent(const std::string &msg);
/// \brief If there is unsent data, retry sending it to the agent.
///
/// Return true if agent has received all messages we sent.
///
/// If the return value is false,
/// you must call flushToAgent again at a later time (e.g.
/// after 50ms, or preferably when the socket is found to be writable.)
bool flushToAgent();
private:
std::string to_agent;
std::string to_client;
int client_socket;
};

View file

@ -0,0 +1,52 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <vector>
class SocketReceiver;
/// \brief
/// Used by LoadBalancer to manage child processes.
///
/// One or more SocketReceiver objects may be used to pass sockets
/// (and/or messages) between master and child processes.
///
/// Each SocketReceiver will be called _a channel_, and they will be
/// referenced using integers starting with 0.
///
/// The point of using channels is to pass different types of connections
/// (e.g. with or without SSL encryption) on different channels.
class WorkerProcess {
public:
/// Create worker to run in newly forked process `pid`.
WorkerProcess(pid_t pid, std::vector<SocketReceiver *> &receivers) :
worker_pid(pid),
channels(receivers) {
}
~WorkerProcess() {
for (auto &conn : channels)
conn->peerDead();
}
/// Return the PID of the worker process.
pid_t pid() const {
return worker_pid;
}
/// Return a channel.
SocketReceiver *channel(unsigned int n=0) const {
return channels.at(n);
}
/// Return number of channels.
size_t noChannels() const {
return channels.size();
}
private:
pid_t worker_pid;
std::vector<SocketReceiver *> channels;
};

1
src/gtkgui/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
bredbandskollen

26
src/gtkgui/Makefile Normal file
View file

@ -0,0 +1,26 @@
TARGET = bredbandskollen
DIRLEVEL = ..
# Possible LOGLEVEL values: dbg, info, warn, err, none
LOGLEVEL=info
# We will use Logger in more than one thread:
THREADS=1
# Uncomment if GnuTLS version 3.5 or later is available
# GNUTLS=1
SOURCES=../http/cookiefile.cpp \
../framework/unixdomainbridge.cpp \
../framework/unixdomainclient.cpp \
../framework/shortmessageconnection.cpp \
main.cpp \
gtkclient.cpp \
../cli/utils.cpp
CXXFLAGS=$(shell pkg-config --cflags gtk+-3.0)
LIBS=$(shell pkg-config --libs gtk+-3.0)
#CXXFLAGS += -g
include $(DIRLEVEL)/measurement/mk.inc

407
src/gtkgui/gtkclient.cpp Normal file
View file

@ -0,0 +1,407 @@
// Copyright (c) 2019 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <stdio.h>
#include "gtkclient.h"
#include <glib-unix.h>
void GtkClient::run() {
app = gtk_application_new("bbk.iis.se", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect(app, "activate",
G_CALLBACK(GtkClient::activate), this);
g_application_run(G_APPLICATION(app), 0, nullptr);
g_object_unref(app);
}
void GtkClient::newEventFromAgent(const std::string &msg) {
log() << "Got: " << msg;
std::string jsonerr;
auto obj = json11::Json::parse(msg, jsonerr);
if (!jsonerr.empty()) {
if (BridgeTask::isAgentTerminatedMessage(msg))
setLabel(label_message, msg);
else {
setLabel(label_message, "JSON error: got " + msg);
err_log() << "JSON error";
}
pushToAgent("terminate");
return;
}
std::string event = obj["event"].string_value();
auto arg_obj = obj["args"];
while (state == MState::MEASURING) {
if (event == "taskProgress") {
std::string tst = arg_obj["task"].string_value();
double val = arg_obj["result"].number_value();
std::string p = myStrFormat(val) + " Mbit/s";
if (tst == "download")
setLabel(label_download, p);
else if (tst == "upload" || tst == "uploadinfo")
setLabel(label_upload, p);
} else if (event == "taskStart") {
} else if (event == "taskComplete") {
gotTaskComplete(arg_obj);
} else if (event == "report") {
gotReport(arg_obj);
} else if (event == "measurementInfo") {
std::string id = arg_obj["MeasurementID"].string_value();
} else {
break;
}
return;
}
if (event == "configuration") {
gotSettings(arg_obj);
} else if (event == "agentReady") {
if (state == MState::IDLE)
pushToAgent("getConfiguration");
else if (state == MState::RESTARTING)
doStartMeasurement();
} else if (event == "measurementList") {
} else if (event == "setInfo") {
gotInfo(arg_obj);
}
}
void GtkClient::reset() {
pushToAgent("resetTest");
setLabel(label_message, "");
got_ticket = false;
user_abort = false;
setLabel(label_ticket, "");
setLabel(label_latency, "");
setLabel(label_download, "");
setLabel(label_upload, "");
setLabel(label_evaluation, "");
}
std::string GtkClient::myStrFormat(double x) {
char strBuf[80];
snprintf(strBuf, sizeof(strBuf), "%7.2f", x);
return std::string(strBuf);
}
void GtkClient::gotReport(const json11::Json &obj) {
setLabel(label_pip, obj["localip"].string_value());
}
void GtkClient::gotTaskComplete(const json11::Json &obj) {
std::string tst = obj["task"].string_value();
auto res = obj["result"].number_value();
if (tst == "global") {
if (got_ticket || user_abort) {
setState(MState::FINISHED);
} else {
setState(MState::ERROR);
pushToAgent("resetTest");
}
} else if (tst == "latency")
setLabel(label_latency, myStrFormat(res) + " ms");
else if (tst == "download")
setLabel(label_download, myStrFormat(res) + " Mbit/s");
else if (tst == "upload")
setLabel(label_upload, myStrFormat(res) + " Mbit/s");
}
void GtkClient::gotInfo(const json11::Json &obj) {
for (auto &p : obj.object_items()) {
std::string attr = p.first;
std::string value = p.second.string_value();
if (attr == "error") {
if (!value.empty())
setLabel(label_message, "Error: " + value);
} else if (attr == "ticket") {
setLabel(label_ticket, value);
got_ticket = true;
} else if (attr == "logText") {
} else if (attr == "msgToUser") {
setLabel(label_message, value);
}
}
}
void GtkClient::gotSettings(const json11::Json &obj) {
if (state == MState::ERROR) {
setState(MState::IDLE);
}
settings = obj;
setLabel(label_date, dateString());
setLabel(label_isp, settings["ispname"].string_value());
setLabel(label_ip, settings["ip"].string_value());
updateServerBox();
}
void GtkClient::setLabel(GtkWidget *widget, const std::string &label) {
gtk_label_set_text(GTK_LABEL(widget), label.c_str());
}
void GtkClient::updateServerBox() {
gtk_combo_box_text_remove_all(GTK_COMBO_BOX_TEXT(server_box));
std::set<std::string> srvName;
std::string mtype(gtk_combo_box_text_get_active_text
(GTK_COMBO_BOX_TEXT(iptype_box)));
bool tls = false;
for (auto srv : settings["servers"].array_items()) {
if (srv["type"].string_value() != mtype)
continue;
if (tls && srv["tlsport"].int_value() == 0)
continue;
std::string name = srv["name"].string_value();
if (srvName.find(name) != srvName.end())
continue;
srvName.insert(name);
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(server_box),
name.c_str());
}
if (srvName.empty())
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(server_box),
"No Server");
gtk_combo_box_set_active(GTK_COMBO_BOX(server_box), 0);
}
gboolean GtkClient::poll_agent(gint , GIOCondition c, gpointer data) {
auto self = reinterpret_cast<GtkClient *>(data);
if (c & G_IO_HUP) {
// Agent gone
self->doQuit();
return FALSE;
}
while (true) {
auto msg = self->pollAgent();
if (msg.empty())
break;
self->newEventFromAgent(msg);
}
return TRUE;
}
GtkClient::GtkClient(const TaskConfig &config, int unix_domain_peer) :
Logger("GUI"),
peer_fd(unix_domain_peer),
ud_client(peer_fd),
the_config(config) {
pushToAgent("clientReady");
}
GtkClient::~GtkClient() {
pushToAgent("terminate");
}
void GtkClient::start_measurement(GtkWidget *, gpointer data) {
auto self = reinterpret_cast<GtkClient *>(data);
self->doStartMeasurement();
}
void GtkClient::update_serverlist(GtkWidget *, gpointer data) {
auto self = reinterpret_cast<GtkClient *>(data);
self->updateServerBox();
}
void GtkClient::doStartMeasurement() {
if (state == MState::MEASURING) {
pushToAgent("abortTest");
user_abort = true;
return;
} else if (state == MState::FINISHED) {
setState(MState::RESTARTING);
return;
} else if (state == MState::ERROR) {
pushToAgent("getConfiguration");
return;
}
setLabel(label_date, dateString());
setState(MState::MEASURING);
std::string mtype(gtk_combo_box_text_get_active_text
(GTK_COMBO_BOX_TEXT(iptype_box)));
bool tls = false;
std::string server_name(gtk_combo_box_text_get_active_text
(GTK_COMBO_BOX_TEXT(server_box)));
int server_port = 80;
std::string hostname;
std::set<std::string> srvName;
for (auto srv : settings["servers"].array_items()) {
if (srv["type"].string_value() != mtype)
continue;
if ( srv["name"].string_value() != server_name)
continue;
if (tls && srv["tlsport"].int_value() == 0)
continue;
hostname = srv["url"].string_value();
auto pos = hostname.find(':');
if (pos != std::string::npos) {
server_port = std::stoi(hostname.substr(pos+1));
hostname.resize(pos);
}
if (tls)
server_port = srv["tlsport"].int_value();
break;
}
std::string key = settings["hashkey"].string_value();
json11::Json out_args = json11::Json::object {
{ "serverUrl", hostname },
{ "serverPort", server_port },
{ "userKey", key },
{ "tls", tls },
};
pushToAgent("startTest", out_args.dump());
}
void GtkClient::setState(MState newState) {
if (state == MState::FINISHED) {
reset();
}
switch (newState) {
case MState::IDLE:
setLabel(label_message, "");
gtk_button_set_label(GTK_BUTTON(start_button), "Start Measurement");
break;
case MState::RESTARTING:
case MState::MEASURING:
gtk_button_set_label(GTK_BUTTON(start_button), "ABORT");
break;
case MState::FINISHED:
gtk_button_set_label(GTK_BUTTON(start_button), "New Measurement");
break;
case MState::ERROR:
gtk_button_set_label(GTK_BUTTON(start_button), "RETRY");
break;
}
state = newState;
}
void GtkClient::activate(GtkApplication *app,
gpointer user_data) {
auto self = reinterpret_cast<GtkClient *>(user_data);
self->doActivate(app);
}
GtkWidget *GtkClient::staticLabel(const char *text, GtkAlign align) {
GtkWidget *label = gtk_label_new(text);
gtk_widget_set_halign(label, align);
gtk_widget_set_name(label, "static-label");
return label;
}
GtkWidget *GtkClient::dynamicLabel(GtkAlign align) {
GtkWidget *label = gtk_label_new("");
gtk_widget_set_halign(label, align);
gtk_widget_set_name(label, "dynamic-label");
return label;
}
void GtkClient::doActivate(GtkApplication *app) {
main_window = gtk_application_window_new(app);
gtk_window_set_title(GTK_WINDOW(main_window), "Bandwidth Measurement");
gtk_window_set_default_size(GTK_WINDOW(main_window), 700, 300);
GdkDisplay *display = gdk_display_get_default();
GdkScreen *screen = gdk_display_get_default_screen(display);
GtkCssProvider *cssProvider = gtk_css_provider_new();
if (gtk_css_provider_load_from_path(cssProvider, "gtkclient.css", nullptr))
gtk_style_context_add_provider_for_screen(screen,
GTK_STYLE_PROVIDER(cssProvider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
GtkWidget *mw = gtk_grid_new();
gtk_widget_set_hexpand(mw, TRUE);
gtk_grid_set_column_homogeneous(GTK_GRID(mw), TRUE);
gtk_container_add(GTK_CONTAINER(main_window), mw);
gtk_grid_set_row_spacing(GTK_GRID(mw), 10);
gtk_grid_set_column_spacing(GTK_GRID(mw), 2);
auto label_ticket1 = staticLabel("Suport ID: ");
gtk_widget_set_halign(label_ticket1, GTK_ALIGN_END);
gtk_grid_attach(GTK_GRID(mw), label_ticket1, 0, 0, 1, 1);
label_ticket = dynamicLabel();
gtk_grid_attach(GTK_GRID(mw), label_ticket, 1, 0, 1, 1);
label_date = dynamicLabel();
gtk_grid_attach(GTK_GRID(mw), label_date, 2, 0, 2, 1);
auto label_isp1 = staticLabel("ISP: ");
gtk_grid_attach(GTK_GRID(mw), label_isp1, 0, 1, 1, 1);
label_isp = dynamicLabel();
gtk_grid_attach(GTK_GRID(mw), label_isp, 1, 1, 3, 1);
GtkWidget *label_ip1 = staticLabel("IP: ");
gtk_grid_attach(GTK_GRID(mw), label_ip1, 0, 2, 1, 1);
label_ip = dynamicLabel();
gtk_grid_attach(GTK_GRID(mw), label_ip, 1, 2, 1, 1);
GtkWidget *label_ip2 = staticLabel("Local IP: ");
gtk_grid_attach(GTK_GRID(mw), label_ip2, 2, 2, 1, 1);
label_pip = dynamicLabel();
gtk_grid_attach(GTK_GRID(mw), label_pip, 3, 2, 1, 1);
int res_line = 3;
label_message = dynamicLabel(GTK_ALIGN_CENTER);
gtk_grid_attach(GTK_GRID(mw), label_message, 0, res_line, 4, 1);
++res_line;
GtkWidget *label_x2 = staticLabel("Latency: ");
label_latency = dynamicLabel();
GtkWidget *label_x3 = staticLabel("Evaluation: ");
label_evaluation = dynamicLabel();
gtk_grid_attach(GTK_GRID(mw), label_x2, 0, res_line, 1, 1);
gtk_grid_attach(GTK_GRID(mw), label_latency, 1, res_line, 1, 1);
gtk_grid_attach(GTK_GRID(mw), label_x3, 2, res_line, 1, 1);
gtk_grid_attach(GTK_GRID(mw), label_evaluation, 3, res_line, 1, 1);
++res_line;
GtkWidget *label_x0 = staticLabel("Download: ");
label_download = dynamicLabel();
GtkWidget *label_x1 = staticLabel("Upload: ");
label_upload = dynamicLabel();
gtk_grid_attach(GTK_GRID(mw), label_x0, 0, res_line, 1, 1);
gtk_grid_attach(GTK_GRID(mw), label_download, 1, res_line, 1, 1);
gtk_grid_attach(GTK_GRID(mw), label_x1, 2, res_line, 1, 1);
gtk_grid_attach(GTK_GRID(mw), label_upload, 3, res_line, 1, 1);
++res_line;
start_button = gtk_button_new_with_label("Start Measurement");
gtk_grid_attach(GTK_GRID(mw), start_button, 1, res_line+4, 2, 1);
g_signal_connect(start_button, "clicked",
G_CALLBACK(start_measurement), this);
GtkWidget *label_server1 = staticLabel("Measurement server: ");
gtk_grid_attach(GTK_GRID(mw), label_server1, 0, res_line+5, 1, 1);
server_box = gtk_combo_box_text_new();
gtk_grid_attach(GTK_GRID(mw), server_box, 1, res_line+5, 1, 1);
iptype_box = gtk_combo_box_text_new();
gtk_grid_attach(GTK_GRID(mw), iptype_box, 2, res_line+5, 1, 1);
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(iptype_box),
"ipv4");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(iptype_box),
"ipv6");
gtk_combo_box_set_active(GTK_COMBO_BOX(iptype_box), 0);
g_signal_connect(iptype_box, "changed",
G_CALLBACK(update_serverlist), this);
auto cond = static_cast<GIOCondition>(G_IO_IN | G_IO_HUP | G_IO_ERR);
g_unix_fd_add(peer_fd, cond, poll_agent, this);
gtk_widget_show_all(main_window);
}

11
src/gtkgui/gtkclient.css Normal file
View file

@ -0,0 +1,11 @@
window {
}
#dynamic-label {
background-color: #ffffff;
font-family: monospace;
}
#static-label {
font-weight: bold;
}

Some files were not shown because too many files have changed in this diff Show more