initial
This commit is contained in:
commit
ab9a0bd4e2
183 changed files with 20701 additions and 0 deletions
BIN
.cache/nix/fetcher-cache-v4.sqlite
Normal file
BIN
.cache/nix/fetcher-cache-v4.sqlite
Normal file
Binary file not shown.
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*.d
|
||||||
|
*.o
|
||||||
|
src/cli/cli
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode
|
||||||
8
LICENSE
Normal file
8
LICENSE
Normal 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
142
README.md
Normal 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
3
doc/Makefile
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
api:
|
||||||
|
cd .. && \
|
||||||
|
doxygen doc/doxygen.cfg
|
||||||
2660
doc/doxygen.cfg
Normal file
2660
doc/doxygen.cfg
Normal file
File diff suppressed because it is too large
Load diff
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal 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
65
flake.nix
Normal 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
1
result
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/nwh5jhgbd3yq2bnryjf1kq6fhbsanm41-bredbandskollen-cli-unstable
|
||||||
2
src/.gitignore
vendored
Normal file
2
src/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.o
|
||||||
|
*.d
|
||||||
1
src/cli/.gitignore
vendored
Normal file
1
src/cli/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
cli
|
||||||
31
src/cli/Makefile
Normal file
31
src/cli/Makefile
Normal 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
455
src/cli/cliclient.cpp
Normal 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
54
src/cli/cliclient.h
Normal 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
56
src/cli/main.cpp
Normal 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
325
src/cli/utils.cpp
Normal 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
11
src/cli/utils.h
Normal 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
10
src/examples/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
boss
|
||||||
|
cliclient
|
||||||
|
client
|
||||||
|
echoclient
|
||||||
|
echoserver
|
||||||
|
threadserver
|
||||||
|
timers
|
||||||
|
webserver
|
||||||
|
winner
|
||||||
|
winnerclient
|
||||||
25
src/examples/001_single_request/Makefile
Normal file
25
src/examples/001_single_request/Makefile
Normal 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
|
||||||
18
src/examples/001_single_request/main.cpp
Normal file
18
src/examples/001_single_request/main.cpp
Normal 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;
|
||||||
|
}
|
||||||
25
src/examples/002_simple_task/Makefile
Normal file
25
src/examples/002_simple_task/Makefile
Normal 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
|
||||||
77
src/examples/002_simple_task/main.cpp
Normal file
77
src/examples/002_simple_task/main.cpp
Normal 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;
|
||||||
|
}
|
||||||
7
src/examples/003_webserver/Makefile
Normal file
7
src/examples/003_webserver/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
TARGET = webserver
|
||||||
|
DIRLEVEL = ../..
|
||||||
|
LOGLEVEL = info
|
||||||
|
GNUTLS = 0
|
||||||
|
SOURCES = main.cpp
|
||||||
|
|
||||||
|
include $(DIRLEVEL)/http/mk.inc
|
||||||
55
src/examples/003_webserver/main.cpp
Normal file
55
src/examples/003_webserver/main.cpp
Normal 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;
|
||||||
|
}
|
||||||
22
src/examples/004_timers/Makefile
Normal file
22
src/examples/004_timers/Makefile
Normal 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
|
||||||
45
src/examples/004_timers/main.cpp
Normal file
45
src/examples/004_timers/main.cpp
Normal 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;
|
||||||
|
}
|
||||||
7
src/examples/005_websocket_server/Makefile
Normal file
7
src/examples/005_websocket_server/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
TARGET = winner
|
||||||
|
DIRLEVEL = ../..
|
||||||
|
LOGLEVEL = info
|
||||||
|
GNUTLS = 0
|
||||||
|
SOURCES = main.cpp winner.cpp
|
||||||
|
|
||||||
|
include $(DIRLEVEL)/http/mk.inc
|
||||||
36
src/examples/005_websocket_server/main.cpp
Normal file
36
src/examples/005_websocket_server/main.cpp
Normal 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;
|
||||||
|
}
|
||||||
43
src/examples/005_websocket_server/winner.cpp
Normal file
43
src/examples/005_websocket_server/winner.cpp
Normal 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));
|
||||||
|
}
|
||||||
30
src/examples/005_websocket_server/winner.h
Normal file
30
src/examples/005_websocket_server/winner.h
Normal 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;
|
||||||
|
};
|
||||||
7
src/examples/006_websocket_client/Makefile
Normal file
7
src/examples/006_websocket_client/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
TARGET = winnerclient
|
||||||
|
DIRLEVEL = ../..
|
||||||
|
LOGLEVEL = info
|
||||||
|
GNUTLS = 0
|
||||||
|
SOURCES = main.cpp winnerclient.cpp
|
||||||
|
|
||||||
|
include $(DIRLEVEL)/http/mk.inc
|
||||||
13
src/examples/006_websocket_client/main.cpp
Normal file
13
src/examples/006_websocket_client/main.cpp
Normal 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;
|
||||||
|
}
|
||||||
37
src/examples/006_websocket_client/main2.cpp
Normal file
37
src/examples/006_websocket_client/main2.cpp
Normal 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;
|
||||||
|
}
|
||||||
49
src/examples/006_websocket_client/winnerclient.cpp
Normal file
49
src/examples/006_websocket_client/winnerclient.cpp
Normal 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;
|
||||||
|
}
|
||||||
18
src/examples/006_websocket_client/winnerclient.h
Normal file
18
src/examples/006_websocket_client/winnerclient.h
Normal 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;
|
||||||
|
};
|
||||||
10
src/examples/007_events/Makefile
Normal file
10
src/examples/007_events/Makefile
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
TARGET = client
|
||||||
|
DIRLEVEL = ../..
|
||||||
|
LOGLEVEL = dbg
|
||||||
|
GNUTLS = 0
|
||||||
|
|
||||||
|
SOURCES = main.cpp
|
||||||
|
|
||||||
|
CXXFLAGS += -g
|
||||||
|
|
||||||
|
include $(DIRLEVEL)/http/mk.inc
|
||||||
78
src/examples/007_events/main.cpp
Normal file
78
src/examples/007_events/main.cpp
Normal 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;
|
||||||
|
}
|
||||||
11
src/examples/008_workers/Makefile
Normal file
11
src/examples/008_workers/Makefile
Normal 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
|
||||||
103
src/examples/008_workers/main.cpp
Normal file
103
src/examples/008_workers/main.cpp
Normal 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;
|
||||||
|
}
|
||||||
12
src/examples/009_threads/Makefile
Normal file
12
src/examples/009_threads/Makefile
Normal 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
|
||||||
63
src/examples/009_threads/main.cpp
Normal file
63
src/examples/009_threads/main.cpp
Normal 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;
|
||||||
|
}
|
||||||
10
src/examples/010_echoserver/Makefile
Normal file
10
src/examples/010_echoserver/Makefile
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
TARGET = echoserver
|
||||||
|
DIRLEVEL = ../..
|
||||||
|
LOGLEVEL = dbg
|
||||||
|
GNUTLS = 0
|
||||||
|
|
||||||
|
SOURCES = echoserverconnection.cpp \
|
||||||
|
echoservertask.cpp \
|
||||||
|
echoserver.cpp
|
||||||
|
|
||||||
|
include $(DIRLEVEL)/framework/mk.inc
|
||||||
23
src/examples/010_echoserver/echoserver.cpp
Normal file
23
src/examples/010_echoserver/echoserver.cpp
Normal 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;
|
||||||
|
}
|
||||||
39
src/examples/010_echoserver/echoserverconnection.cpp
Normal file
39
src/examples/010_echoserver/echoserverconnection.cpp
Normal 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;
|
||||||
|
}
|
||||||
19
src/examples/010_echoserver/echoserverconnection.h
Normal file
19
src/examples/010_echoserver/echoserverconnection.h
Normal 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:
|
||||||
|
};
|
||||||
52
src/examples/010_echoserver/echoservertask.cpp
Normal file
52
src/examples/010_echoserver/echoservertask.cpp
Normal 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!");
|
||||||
|
}
|
||||||
39
src/examples/010_echoserver/echoservertask.h
Normal file
39
src/examples/010_echoserver/echoservertask.h
Normal 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;
|
||||||
|
};
|
||||||
10
src/examples/011_echoclient/Makefile
Normal file
10
src/examples/011_echoclient/Makefile
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
TARGET = echoclient
|
||||||
|
DIRLEVEL = ../..
|
||||||
|
LOGLEVEL = dbg
|
||||||
|
GNUTLS = 0
|
||||||
|
|
||||||
|
SOURCES = echoclientconnection.cpp \
|
||||||
|
echoclienttask.cpp \
|
||||||
|
echoclient.cpp
|
||||||
|
|
||||||
|
include $(DIRLEVEL)/framework/mk.inc
|
||||||
14
src/examples/011_echoclient/echoclient.cpp
Normal file
14
src/examples/011_echoclient/echoclient.cpp
Normal 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;
|
||||||
|
}
|
||||||
36
src/examples/011_echoclient/echoclientconnection.cpp
Normal file
36
src/examples/011_echoclient/echoclientconnection.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/examples/011_echoclient/echoclientconnection.h
Normal file
17
src/examples/011_echoclient/echoclientconnection.h
Normal 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;
|
||||||
|
};
|
||||||
20
src/examples/011_echoclient/echoclienttask.cpp
Normal file
20
src/examples/011_echoclient/echoclienttask.cpp
Normal 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;
|
||||||
|
}
|
||||||
35
src/examples/011_echoclient/echoclienttask.h
Normal file
35
src/examples/011_echoclient/echoclienttask.h
Normal 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;
|
||||||
|
};
|
||||||
9
src/examples/020_cliclient/Makefile
Normal file
9
src/examples/020_cliclient/Makefile
Normal 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
|
||||||
98
src/examples/020_cliclient/cliclient.cpp
Normal file
98
src/examples/020_cliclient/cliclient.cpp
Normal 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;
|
||||||
|
}
|
||||||
57
src/examples/020_cliclient/webserver.cpp
Normal file
57
src/examples/020_cliclient/webserver.cpp
Normal 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;
|
||||||
|
}
|
||||||
23
src/examples/020_cliclient/webserver.h
Normal file
23
src/examples/020_cliclient/webserver.h
Normal 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;
|
||||||
|
};
|
||||||
11
src/examples/030_httpclient/Makefile
Normal file
11
src/examples/030_httpclient/Makefile
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
TARGET = client
|
||||||
|
DIRLEVEL = ../..
|
||||||
|
LOGLEVEL = dbg
|
||||||
|
GNUTLS = 0
|
||||||
|
THREADS = 0
|
||||||
|
|
||||||
|
SOURCES = main.cpp
|
||||||
|
|
||||||
|
CXXFLAGS += -g
|
||||||
|
|
||||||
|
include $(DIRLEVEL)/http/mk.inc
|
||||||
76
src/examples/030_httpclient/main.cpp
Normal file
76
src/examples/030_httpclient/main.cpp
Normal 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
15
src/examples/Makefile
Normal 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
|
||||||
20
src/framework/bridgetask.cpp
Normal file
20
src/framework/bridgetask.cpp
Normal 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
142
src/framework/bridgetask.h
Normal 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
644
src/framework/engine.cpp
Normal 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
151
src/framework/engine.h
Normal 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
726
src/framework/eventloop.cpp
Normal 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
308
src/framework/eventloop.h
Normal 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;
|
||||||
|
};
|
||||||
161
src/framework/loadbalancer.cpp
Normal file
161
src/framework/loadbalancer.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/framework/loadbalancer.h
Normal file
78
src/framework/loadbalancer.h
Normal 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
179
src/framework/logger.cpp
Normal 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
382
src/framework/logger.h
Normal 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
140
src/framework/mk.inc
Normal 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
72
src/framework/msgqueue.h
Normal 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
25
src/framework/pollstate.h
Normal 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. */
|
||||||
|
};
|
||||||
51
src/framework/serversocket.cpp
Normal file
51
src/framework/serversocket.cpp
Normal 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;
|
||||||
|
}
|
||||||
58
src/framework/serversocket.h
Normal file
58
src/framework/serversocket.h
Normal 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
|
||||||
|
};
|
||||||
68
src/framework/shortmessageconnection.cpp
Normal file
68
src/framework/shortmessageconnection.cpp
Normal 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);
|
||||||
|
}
|
||||||
47
src/framework/shortmessageconnection.h
Normal file
47
src/framework/shortmessageconnection.h
Normal 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
370
src/framework/socket.cpp
Normal 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
237
src/framework/socket.h
Normal 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();
|
||||||
|
};
|
||||||
327
src/framework/socketconnection.cpp
Normal file
327
src/framework/socketconnection.cpp
Normal 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
|
||||||
327
src/framework/socketconnection.h
Normal file
327
src/framework/socketconnection.h
Normal 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
|
||||||
|
};
|
||||||
103
src/framework/socketreceiver.cpp
Normal file
103
src/framework/socketreceiver.cpp
Normal 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
|
||||||
59
src/framework/socketreceiver.h
Normal file
59
src/framework/socketreceiver.h
Normal 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;
|
||||||
|
};
|
||||||
27
src/framework/synchronousbridge.cpp
Normal file
27
src/framework/synchronousbridge.cpp
Normal 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();
|
||||||
|
}
|
||||||
69
src/framework/synchronousbridge.h
Normal file
69
src/framework/synchronousbridge.h
Normal 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
146
src/framework/task.cpp
Normal 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
519
src/framework/task.h
Normal 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;
|
||||||
|
};
|
||||||
196
src/framework/taskconfig.cpp
Normal file
196
src/framework/taskconfig.cpp
Normal 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
161
src/framework/taskconfig.h
Normal 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);
|
||||||
42
src/framework/threadbridge.cpp
Normal file
42
src/framework/threadbridge.cpp
Normal 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);
|
||||||
|
}
|
||||||
37
src/framework/threadbridge.h
Normal file
37
src/framework/threadbridge.h
Normal 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;
|
||||||
|
};
|
||||||
42
src/framework/unixdomainbridge.cpp
Normal file
42
src/framework/unixdomainbridge.cpp
Normal 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;
|
||||||
|
}
|
||||||
49
src/framework/unixdomainbridge.h
Normal file
49
src/framework/unixdomainbridge.h
Normal 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;
|
||||||
|
};
|
||||||
87
src/framework/unixdomainclient.cpp
Normal file
87
src/framework/unixdomainclient.cpp
Normal 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();
|
||||||
|
}
|
||||||
62
src/framework/unixdomainclient.h
Normal file
62
src/framework/unixdomainclient.h
Normal 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;
|
||||||
|
};
|
||||||
52
src/framework/workerprocess.h
Normal file
52
src/framework/workerprocess.h
Normal 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
1
src/gtkgui/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
bredbandskollen
|
||||||
26
src/gtkgui/Makefile
Normal file
26
src/gtkgui/Makefile
Normal 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
407
src/gtkgui/gtkclient.cpp
Normal 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
11
src/gtkgui/gtkclient.css
Normal 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
Loading…
Add table
Add a link
Reference in a new issue