initial
This commit is contained in:
commit
ab9a0bd4e2
183 changed files with 20701 additions and 0 deletions
237
src/measurement/defs.cpp
Normal file
237
src/measurement/defs.cpp
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <sys/types.h>
|
||||
#include <sys/sysctl.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#endif
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <intrin.h>
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
|
||||
#include "defs.h"
|
||||
#include "../http/httpclientconnection.h"
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef __APPLE__
|
||||
#elif defined(NO_EXTERNAL_CMD)
|
||||
std::string get_shell_value(std::string filename, std::string key) {
|
||||
std::ifstream file(filename);
|
||||
if (file) {
|
||||
key += "=";
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
if (line.find(key) == 0) {
|
||||
std::string value = line.substr(key.size());
|
||||
if (value.size() > 1 && value[0] == '"' &&
|
||||
value[value.size() - 1] == '"') {
|
||||
return value.substr(1, value.size() - 2);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::string get_os_identifier() {
|
||||
std::string os_name = get_shell_value("/etc/os-release", "NAME");
|
||||
if (!os_name.empty()) {
|
||||
std::string os_version = get_shell_value("/etc/os-release", "VERSION");
|
||||
if (!os_version.empty()) {
|
||||
os_name += " " + os_version;
|
||||
}
|
||||
}
|
||||
return os_name;
|
||||
}
|
||||
|
||||
std::string get_arch_identifier() {
|
||||
#if defined(__aarch64__)
|
||||
return "aarch64";
|
||||
#elif defined(__arm__) || defined(_M_ARM)
|
||||
return "arm";
|
||||
#elif defined(__amd64__) || defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64)
|
||||
return "x86_64";
|
||||
#elif defined(__i386) || defined(_M_IX86) || defined(__X86__) || defined(_X86_)
|
||||
return "i386";
|
||||
#elif defined(mips) || defined(__mips__) || defined(__mips)
|
||||
return "mips";
|
||||
#elif defined(__PPC64__) || defined(__ppc64__) || defined(_ARCH_PPC64)
|
||||
return "powerpc64"
|
||||
#elif defined(__powerpc) || defined(__powerpc__) || defined(__powerpc64__) || defined(__POWERPC__) || defined(__ppc__) || defined(__PPC__) || defined(_ARCH_PPC)
|
||||
return "powerpc";
|
||||
#else
|
||||
return std::string();
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
std::string external_cmd(const char *cmd) {
|
||||
char buffer[200];
|
||||
std::string result;
|
||||
#ifdef _WIN32
|
||||
std::shared_ptr<FILE> pipe(_popen(cmd, "r"), _pclose);
|
||||
#else
|
||||
std::shared_ptr<FILE> pipe(popen(cmd, "r"), pclose);
|
||||
#endif
|
||||
if (pipe) {
|
||||
while (!feof(pipe.get()))
|
||||
if (fgets(buffer, sizeof buffer, pipe.get()) != nullptr)
|
||||
result += buffer;
|
||||
auto pos = result.find_last_not_of(" \t\r\n");
|
||||
if (pos != std::string::npos)
|
||||
result.resize(pos+1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string app_name() {
|
||||
|
||||
std::string appName = "Bredbandskollen";
|
||||
|
||||
#ifdef _WIN32
|
||||
appName += " Windows";
|
||||
#elif defined(__ANDROID__)
|
||||
appName += " Android";
|
||||
#elif defined(__APPLE__)
|
||||
appName += " Mac";
|
||||
#elif defined(__FreeBSD__)
|
||||
appName += " FreeBSD";
|
||||
#elif defined(__NetBSD__)
|
||||
appName += " NetBSD";
|
||||
#elif defined(__OpenBSD__)
|
||||
appName += " OpenBSD";
|
||||
#elif defined(__bsdi__)
|
||||
appName += " BSDi";
|
||||
#elif defined(__DragonFly__)
|
||||
appName += " DragonFly";
|
||||
#else
|
||||
appName += " Linux";
|
||||
#endif
|
||||
|
||||
#ifdef __arm__
|
||||
return appName + " ARM";
|
||||
#elif defined(_M_ARM)
|
||||
return appName + " ARM";
|
||||
#elif defined(__aarch64__)
|
||||
return appName + " ARM64";
|
||||
#elif defined(__i386)
|
||||
return appName + " i386";
|
||||
#elif defined(_M_IX86)
|
||||
return appName + " i386";
|
||||
#elif defined(__X86__)
|
||||
return appName + " i386";
|
||||
#elif defined(_X86_)
|
||||
return appName + " i386";
|
||||
#elif defined(__amd64__)
|
||||
return appName + " amd64";
|
||||
#elif defined(__x86_64__)
|
||||
return appName + " amd64";
|
||||
#elif defined(_M_X64)
|
||||
return appName + " amd64";
|
||||
#elif defined(_M_AMD64)
|
||||
return appName + " amd64";
|
||||
#elif defined(__mips__)
|
||||
return appName + " mips";
|
||||
#elif defined(__mips)
|
||||
return appName + " mips";
|
||||
#else
|
||||
return appName;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string getHWModel() {
|
||||
#ifdef __APPLE__
|
||||
size_t size;
|
||||
char array[1000];
|
||||
sysctlbyname("hw.model", NULL, &size, NULL, 0);
|
||||
sysctlbyname("hw.model", &array, &size, NULL, 0);
|
||||
return array;
|
||||
#elif defined(_WIN32)
|
||||
// Get extended ids.
|
||||
int CPUInfo[4] = { -1 };
|
||||
__cpuid(CPUInfo, 0x80000000);
|
||||
unsigned int nExIds = CPUInfo[0];
|
||||
|
||||
// Get the information associated with each extended ID.
|
||||
char CPUBrandString[0x40] = { 0 };
|
||||
for (unsigned int i = 0x80000000; i <= nExIds; ++i) {
|
||||
__cpuid(CPUInfo, i);
|
||||
// Interpret CPU brand string and cache information.
|
||||
if (i == 0x80000002)
|
||||
memcpy(CPUBrandString, CPUInfo, sizeof(CPUInfo)); // "Intel(R) Core(TM)"
|
||||
else if (i == 0x80000003)
|
||||
memcpy(CPUBrandString + 16, CPUInfo, sizeof(CPUInfo)); // "i7-4710HQ CPU"
|
||||
else if (i == 0x80000004)
|
||||
memcpy(CPUBrandString + 32, CPUInfo, sizeof(CPUInfo)); // "@ 2.50GHz"
|
||||
}
|
||||
return CPUBrandString;
|
||||
#elif defined(NO_EXTERNAL_CMD)
|
||||
return get_arch_identifier();
|
||||
#else
|
||||
return external_cmd("uname -m").substr(0, 50);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string getOSInfo() {
|
||||
std::string osInfo = "";
|
||||
#ifdef _WIN32
|
||||
osInfo = "Windows ";
|
||||
|
||||
NTSTATUS(WINAPI *RtlGetVersion)(LPOSVERSIONINFOEXW);
|
||||
OSVERSIONINFOEXW osVerInfo;
|
||||
|
||||
*(FARPROC*)&RtlGetVersion = GetProcAddress(GetModuleHandleA("ntdll"), "RtlGetVersion");
|
||||
|
||||
if (NULL != RtlGetVersion) {
|
||||
osVerInfo.dwOSVersionInfoSize = sizeof(osVerInfo);
|
||||
RtlGetVersion(&osVerInfo);
|
||||
osInfo += std::to_string(osVerInfo.dwMajorVersion) + "." + std::to_string(osVerInfo.dwMinorVersion);
|
||||
if (osVerInfo.szCSDVersion[0] != 0) {
|
||||
osInfo += " ";
|
||||
int i = 0;
|
||||
while (osVerInfo.szCSDVersion[i] != 0) {
|
||||
osInfo += osVerInfo.szCSDVersion[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (osVerInfo.wSuiteMask | VER_SUITE_PERSONAL) {
|
||||
osInfo += " Home";
|
||||
}
|
||||
osInfo += " (Build " + std::to_string(osVerInfo.dwBuildNumber) + ")";
|
||||
}
|
||||
else {
|
||||
osInfo += "unknown";
|
||||
}
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
osInfo = [[[NSProcessInfo processInfo] operatingSystemVersionString] UTF8String];
|
||||
|
||||
#elif defined(NO_EXTERNAL_CMD)
|
||||
osInfo = get_os_identifier();
|
||||
|
||||
#else
|
||||
osInfo = external_cmd("uname -sr").substr(0, 50);
|
||||
#endif
|
||||
return osInfo;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string measurement::appName = app_name();
|
||||
#if defined(__ANDROID__)
|
||||
const std::string measurement::hw_info = "Android Device";
|
||||
const std::string measurement::os_version = "Android OS";
|
||||
#else
|
||||
const std::string measurement::hw_info = getHWModel();
|
||||
const std::string measurement::os_version = getOSInfo();
|
||||
#endif
|
||||
12
src/measurement/defs.h
Normal file
12
src/measurement/defs.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace measurement {
|
||||
extern const std::string appName;
|
||||
const std::string appVersion = "1.2.2";
|
||||
// buildVersion 15 released to App Store in 2016.
|
||||
const unsigned int buildVersion = 16;
|
||||
extern const std::string hw_info;
|
||||
extern const std::string os_version;
|
||||
}
|
||||
68
src/measurement/downloadtask.cpp
Normal file
68
src/measurement/downloadtask.cpp
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "downloadtask.h"
|
||||
|
||||
DownloadTask::DownloadTask(const std::string &ticket, const HttpHost &server,
|
||||
unsigned int no_conn, unsigned int max_conn,
|
||||
double duration, double max_time, double tick_s) :
|
||||
ProgressTask("download", ticket, server, no_conn, max_conn, duration, max_time),
|
||||
tick_duration_s(tick_s>0 ? tick_s : 0.1) {
|
||||
dynamic_conn_limit = 0.5 * duration + 0.5;
|
||||
}
|
||||
|
||||
double DownloadTask::start() {
|
||||
log() << "DownloadTask starting";
|
||||
notifyStarted();
|
||||
checkConnectionCount();
|
||||
return tick_duration_s;
|
||||
}
|
||||
|
||||
double DownloadTask::timerEvent() {
|
||||
double time = elapsed();
|
||||
|
||||
if (time >= timeout_s()) {
|
||||
log() << "DownloadTask timeout after " << time << " seconds";
|
||||
setResult("-1");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (time > 0.05) {
|
||||
// log() << "Check: " << byteCount() << " and " << threadRecvCount();
|
||||
double speed = addOverheadMbps(threadRecvCount(), time);
|
||||
if (time < dynamic_conn_limit && speed >= 50.0) {
|
||||
unsigned int no_conn = (speed < 250.0) ?
|
||||
((speed < 100) ? 12 : 24) :
|
||||
((speed < 500) ? 32 : 48);
|
||||
if (no_conn < currentNoConnections())
|
||||
no_conn = currentNoConnections();
|
||||
doTestProgress(speed, time, no_conn);
|
||||
setNoConnections(no_conn);
|
||||
} else {
|
||||
doTestProgress(speed, time, currentNoConnections());
|
||||
}
|
||||
}
|
||||
return tick_duration_s;
|
||||
}
|
||||
|
||||
void DownloadTask::newRequest(HttpClientConnection *conn) {
|
||||
if (size_t size = loadSize()) {
|
||||
conn->get("/bigfile.bin?t=" + t() + "&len=" + std::to_string(size));
|
||||
} else if (soonFinished()) {
|
||||
// Delete the Connection object but let socket go to keep-alive cache.
|
||||
} else {
|
||||
// Keep the connection but don't make a new request at this point.
|
||||
// Perhaps due to a speed limit.
|
||||
conn->pass();
|
||||
}
|
||||
}
|
||||
|
||||
bool DownloadTask::headerComplete(HttpClientConnection *conn) {
|
||||
conn->doStreamResponse();
|
||||
notifyBytesLoaded(conn->rawHttpHeader().size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void DownloadTask::payload(HttpClientConnection *, char *, size_t len) {
|
||||
notifyBytesLoaded(len);
|
||||
}
|
||||
22
src/measurement/downloadtask.h
Normal file
22
src/measurement/downloadtask.h
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "progresstask.h"
|
||||
|
||||
class DownloadTask : public ProgressTask {
|
||||
public:
|
||||
DownloadTask(const std::string &ticket, const HttpHost &server,
|
||||
unsigned int no_conn = 10, unsigned int max_conn = 100,
|
||||
double duration = 10.0, double max_time = 25.0,
|
||||
double tick_s = 0.1);
|
||||
double start() override;
|
||||
double timerEvent() override;
|
||||
void newRequest(HttpClientConnection *) override;
|
||||
bool headerComplete(HttpClientConnection *) override;
|
||||
void payload(HttpClientConnection *, char *, size_t len) override;
|
||||
private:
|
||||
double tick_duration_s;
|
||||
double dynamic_conn_limit;
|
||||
};
|
||||
45
src/measurement/infotask.cpp
Normal file
45
src/measurement/infotask.cpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "infotask.h"
|
||||
|
||||
void InfoTask::newRequest(HttpClientConnection *conn) {
|
||||
conn->ws_get("/minfo?t=" + t() + "&key=" + key);
|
||||
}
|
||||
|
||||
bool InfoTask::wsTextMessage(HttpConnection *,
|
||||
const std::string &msg) {
|
||||
log() << "MEASUREMENT INFO: " << msg;
|
||||
setMessage(msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
double InfoTask::timerEvent() {
|
||||
double time = elapsed();
|
||||
log() << "IT TimerEvent " << elapsed();
|
||||
if (time >= timeout_s()) {
|
||||
log() << "InfoTask timeout after " << time << " seconds";
|
||||
setResult("");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (upload_deadline > 0.0) {
|
||||
log() << "waiting for server upload, t=" << time << ", max="
|
||||
<< upload_deadline;
|
||||
if (time >= upload_deadline) {
|
||||
setMessage("server upload timeout");
|
||||
upload_deadline = -1.0;
|
||||
}
|
||||
return 0.1;
|
||||
} else if (info_deadline > 0.0) {
|
||||
log() << "waiting for measurement info, t=" << time << ", max="
|
||||
<< info_deadline;
|
||||
if (time >= info_deadline) {
|
||||
setResult("");
|
||||
info_deadline = -1.0;
|
||||
}
|
||||
return 0.1;
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
46
src/measurement/infotask.h
Normal file
46
src/measurement/infotask.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "measurementtask.h"
|
||||
|
||||
class InfoTask : public MeasurementTask {
|
||||
public:
|
||||
InfoTask(const std::string &label, const std::string &ticket_string,
|
||||
const std::string &hash_key, const HttpHost &server,
|
||||
unsigned int no_conn = 1, unsigned int max_conn = 3,
|
||||
double duration = 300.0) :
|
||||
MeasurementTask(label, ticket_string, server,
|
||||
no_conn, max_conn, duration),
|
||||
key(hash_key)
|
||||
{
|
||||
}
|
||||
void newRequest(HttpClientConnection *) override;
|
||||
bool requestComplete(HttpClientConnection *) override {
|
||||
return false;
|
||||
}
|
||||
bool wsTextMessage(HttpConnection *,
|
||||
const std::string &msg) override;
|
||||
double timerEvent() override;
|
||||
|
||||
// Stop waiting for upload result after at most t seconds:
|
||||
void setUploadDeadline(double t) {
|
||||
if (t > 0.0)
|
||||
upload_deadline = elapsed() + t;
|
||||
else
|
||||
upload_deadline = t;
|
||||
}
|
||||
|
||||
// Stop waiting for measurement ID after at most t seconds:
|
||||
void setInfoDeadline(double t) {
|
||||
if (t > 0.0)
|
||||
info_deadline = elapsed() + t;
|
||||
else
|
||||
info_deadline = t;
|
||||
}
|
||||
private:
|
||||
std::string key;
|
||||
double upload_deadline = -1.0;
|
||||
double info_deadline = -1.0;
|
||||
};
|
||||
34
src/measurement/latencytask.cpp
Normal file
34
src/measurement/latencytask.cpp
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#include <algorithm>
|
||||
#include "../json11/json11.hpp"
|
||||
#include "latencytask.h"
|
||||
|
||||
LatencyTask::LatencyTask(const std::string &ticket, const HttpHost &server) :
|
||||
MeasurementTask("httplatency", ticket, server, 1, 15) {
|
||||
}
|
||||
|
||||
void LatencyTask::newRequest(HttpClientConnection *conn) {
|
||||
std::string lbl = std::to_string(++serial_no);
|
||||
current_request[lbl + " ok"] = timeNow();
|
||||
conn->get("/pingpong/" + lbl + "?t=" + t());
|
||||
}
|
||||
|
||||
bool LatencyTask::requestComplete(HttpClientConnection *conn) {
|
||||
auto p = current_request.find(conn->contents());
|
||||
if (p == current_request.end()) {
|
||||
log() << "unexpected response: " << conn->contents();
|
||||
} else {
|
||||
double latency = secondsSince(p->second);
|
||||
log() << "got " << conn->contents() << " after " << latency << " sec";
|
||||
samples.push_back(latency);
|
||||
}
|
||||
|
||||
if (samples.size() < 12)
|
||||
return true;
|
||||
|
||||
if (!terminated()) {
|
||||
log() << "Samples: " << json11::Json(samples).dump();
|
||||
setResult(calculateLatency(samples));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
16
src/measurement/latencytask.h
Normal file
16
src/measurement/latencytask.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "measurementtask.h"
|
||||
|
||||
class LatencyTask : public MeasurementTask {
|
||||
public:
|
||||
LatencyTask(const std::string &ticket, const HttpHost &server);
|
||||
void newRequest(HttpClientConnection *) override;
|
||||
bool requestComplete(HttpClientConnection *conn) override;
|
||||
private:
|
||||
std::vector<double> samples;
|
||||
// Maps expected response to start time of request:
|
||||
std::map<std::string, TimePoint> current_request;
|
||||
unsigned int serial_no = static_cast<unsigned int>(rand());
|
||||
};
|
||||
604
src/measurement/measurementagent.cpp
Normal file
604
src/measurement/measurementagent.cpp
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <clocale>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
|
||||
#include "speedtest.h"
|
||||
|
||||
#include "defs.h"
|
||||
#include "singlerequesttask.h"
|
||||
#include "../http/singlerequest.h"
|
||||
#include "pingsweeptask.h"
|
||||
#include "measurementagent.h"
|
||||
|
||||
MeasurementAgent::MeasurementAgent(const TaskConfig &config,
|
||||
const HttpHost &webserver) :
|
||||
Task("MeasurementAgent"),
|
||||
wserv(webserver) {
|
||||
|
||||
killChildTaskWhenFinished();
|
||||
|
||||
key_store = wserv.cmgr ? wserv.cmgr : new CookieManager();
|
||||
|
||||
// Default values; could be changed by the client app (gui):
|
||||
report_template["appname"] = measurement::appName;
|
||||
report_template["appver"] = measurement::appVersion;
|
||||
report_template["dlength"] = "10";
|
||||
report_template["ulength"] = "10";
|
||||
|
||||
options_filename = config.value("options_file");
|
||||
if (!options_filename.empty()) {
|
||||
// Load preexisting configuration options
|
||||
cfgOptions = cfgOptions.loadJsonFromFile(options_filename);
|
||||
//if (cfgOptions.hasKey("Client.hashkey"))
|
||||
// handleConfigurationOption("Client.hashkey",
|
||||
// cfgOptions.value("Client.hashkey"));
|
||||
}
|
||||
|
||||
for (auto p : config.cfg())
|
||||
handleConfigurationOption(p.first, p.second);
|
||||
}
|
||||
|
||||
void MeasurementAgent::taskMessage(Task *task) {
|
||||
// std::string name = task->label(), message = task->message();
|
||||
if (PingSweepTask *ptask = dynamic_cast<PingSweepTask *>(task)) {
|
||||
while (true) {
|
||||
std::string res = ptask->getResult();
|
||||
if (res.empty())
|
||||
break;
|
||||
sendToClient("setInfo", MeasurementTask::
|
||||
json_obj("approxLatency", res));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MeasurementAgent::taskFinished(Task *task) {
|
||||
std::string name = task->label(),
|
||||
result = task->result();
|
||||
|
||||
if (task->wasKilled())
|
||||
log() << "Task " << name << " killed";
|
||||
else
|
||||
log() << "Task " << name << " finished, result: " << result;
|
||||
|
||||
if (task == bridge) {
|
||||
bridge = nullptr;
|
||||
state = MeasurementState::ABORTED;
|
||||
setResult("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (task == current_test) {
|
||||
current_test = nullptr;
|
||||
state = MeasurementState::FINISHED;
|
||||
sendTaskComplete("global");
|
||||
return;
|
||||
}
|
||||
|
||||
if (task->wasKilled() || !bridge || state == MeasurementState::ABORTED)
|
||||
return;
|
||||
|
||||
if (name == "msettings") {
|
||||
// Check that the result is valid JSON:
|
||||
std::string err;
|
||||
auto obj = json11::Json::parse(result, err);
|
||||
if (result.empty() || !err.empty()) {
|
||||
settings_result = getDefaultConfig();
|
||||
} else {
|
||||
if (result[result.size()-1] == '}') {
|
||||
std::string newkey = obj["hashkey"].string_value();
|
||||
std::string hashkey = !force_key.empty() ? force_key :
|
||||
key_store->getCookieVal("hash_key", wserv.hostname);
|
||||
if (newkey == hashkey) {
|
||||
// Nothing changed.
|
||||
} else if (isValidHashkey(hashkey)) {
|
||||
// We already had a key. Try to insert it into settings.
|
||||
if (newkey.empty()) {
|
||||
result.resize(result.size()-1);
|
||||
((result += ",\"hashkey\":\"") += hashkey) += "\"}";
|
||||
} else {
|
||||
auto pos = result.find('"' + newkey + '"');
|
||||
if (pos != std::string::npos)
|
||||
result.replace(pos+1, newkey.size(), hashkey);
|
||||
}
|
||||
} else if (isValidHashkey(newkey)) {
|
||||
// We had no valid key. Save the one we got.
|
||||
std::string domain = wserv.hostname;
|
||||
if (domain.size() >= 18 && domain.substr(domain.size() - 18)
|
||||
== "bredbandskollen.se")
|
||||
domain = ".bredbandskollen.se";
|
||||
std::string line = "hash_key=" + newkey +
|
||||
"; max-age=999999999; domain=" + domain;
|
||||
key_store->setCookie(line, wserv.hostname, "/");
|
||||
log() << "SET COOKIE " << line;
|
||||
}
|
||||
}
|
||||
settings_result = result;
|
||||
if (!obj["x_bbk"].string_value().empty())
|
||||
report_template["x_bbk"] = obj["x_bbk"].string_value();
|
||||
}
|
||||
sendToClient("configuration", settings_result);
|
||||
key_store->save();
|
||||
} else if (name == "contents") {
|
||||
sendToClient("setInfo", "{\"contents\": " +
|
||||
result + "}");
|
||||
} else if (name == "pingsweep") {
|
||||
log() << "Best server: " << result;
|
||||
sendToClient("setInfo", "{\"bestServer\": \"" + result + "\"}");
|
||||
} else if (name == "list_measurements") {
|
||||
sendToClient("measurementList", result);
|
||||
} else if (name == "checkHost") {
|
||||
if (HttpClientTask *t = dynamic_cast<HttpClientTask *>(task)) {
|
||||
sendToClient("hostCheck", MeasurementTask::
|
||||
json_obj(t->serverHost(), result.empty() ? "" : "1"));
|
||||
log() << "checkHost: " << result;
|
||||
}
|
||||
} else if (name == "measurementStart") {
|
||||
std::string err;
|
||||
auto obj = json11::Json::parse(result, err);
|
||||
|
||||
if (err.empty()) {
|
||||
sendToClient("setInfo", result);
|
||||
log() << "measurementStart: " << result;
|
||||
} else {
|
||||
sendToClient("setInfo", "{}");
|
||||
warn_log() << "invalid measurementStart: " << result;
|
||||
}
|
||||
} else if (name == "setSubscription") {
|
||||
log() << "setSubscription done";
|
||||
} else if (name == "sendLogToServer") {
|
||||
std::string err;
|
||||
auto obj = json11::Json::parse(result, err);
|
||||
std::string res = "NOK";
|
||||
if (err.empty() && obj["status"].number_value() != 0)
|
||||
res = "OK";
|
||||
sendToClient("setInfo", "{\"logSent\": \"" +
|
||||
res + "\"}");
|
||||
} else {
|
||||
log() << "unknown task, ignoring";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MeasurementAgent::pollBridge(const std::string &msg) {
|
||||
if (msg.empty())
|
||||
return;
|
||||
std::string err;
|
||||
auto obj = json11::Json::parse(msg, err);
|
||||
if (!err.empty()) {
|
||||
err_log() << "JSON error, ignoring message: " << msg;
|
||||
return;
|
||||
}
|
||||
std::string method = obj["method"].string_value();
|
||||
const json11::Json &args = obj["args"];
|
||||
handleMsgFromClient(method, args);
|
||||
}
|
||||
|
||||
void MeasurementAgent::sendTaskComplete(const std::string &t,
|
||||
const std::string &res) {
|
||||
if (!bridge)
|
||||
return;
|
||||
std::string args = "{\"task\": \"" + t + "\"";
|
||||
if (!res.empty())
|
||||
args += ", \"result\": " + res;
|
||||
sendToClient("taskComplete", args + "}");
|
||||
}
|
||||
|
||||
bool MeasurementAgent::isValidHashkey(const std::string &key) {
|
||||
if (key.size() >= 12 && key.size() <= 40 && std::string::npos ==
|
||||
key.find_first_not_of("0123456789ABCDEFabcdef-"))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void MeasurementAgent::sendLogToServer() {
|
||||
std::string url = "/api/devlog";
|
||||
addNewTask(new SingleRequest("sendLogToServer", "frontend-beta.bredbandskollen.se",
|
||||
url, accumulated_log.str(), 10.0), this);
|
||||
setLogLimit();
|
||||
log() << "Sent " << accumulated_log.str().size() << " bytes.";
|
||||
accumulated_log.str("");
|
||||
}
|
||||
|
||||
std::string MeasurementAgent::getDefaultConfig() {
|
||||
// Either the client doesn't want settings to be fetched
|
||||
// (wserv.hostname is "none"), or we failed to fetch settings.
|
||||
// If the client has supplied a measurement server, or if the domain is
|
||||
// bredbandskollen.se, we can create a valid config.
|
||||
|
||||
std::string mserver4 = mserv.hostname, mserver6 = mserv.hostname,
|
||||
domain = wserv.hostname;
|
||||
if (domain == "none") {
|
||||
domain = mserv.hostname;
|
||||
} else if (domain.size() >= 18 && domain.substr(domain.size() - 18)
|
||||
== "bredbandskollen.se" && mserver4.empty()) {
|
||||
// Perhaps a client dns failure.
|
||||
mserver4 = "192.36.30.2";
|
||||
mserver6 = "2001:67c:2ff4::2";
|
||||
}
|
||||
std::string hashkey = !force_key.empty() ? force_key :
|
||||
key_store->getCookieVal("hash_key", domain);
|
||||
if (hashkey.empty()) {
|
||||
hashkey = createHashKey();
|
||||
key_store->setCookie("hash_key="+hashkey+"; max-age=999999999",
|
||||
domain, "/");
|
||||
}
|
||||
|
||||
if (mserver4.empty()) {
|
||||
json11::Json settings = json11::Json::object {
|
||||
{ "ispname", "" },
|
||||
{ "hashkey", hashkey }
|
||||
};
|
||||
return settings.dump();
|
||||
}
|
||||
|
||||
json11::Json settings = json11::Json::object {
|
||||
{ "servers", json11::Json::array {
|
||||
json11::Json::object {
|
||||
{ "url", mserver4 },
|
||||
{ "name", mserver4 },
|
||||
{ "type", "ipv4" }
|
||||
},
|
||||
json11::Json::object {
|
||||
{ "url", mserver6 },
|
||||
{ "name", mserver6 },
|
||||
{ "type", "ipv6" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "ispname", "" },
|
||||
{ "hashkey", hashkey }
|
||||
};
|
||||
return settings.dump();
|
||||
}
|
||||
|
||||
void MeasurementAgent::handleMsgFromClient(const std::string &method,
|
||||
const json11::Json &args) {
|
||||
log() << "handleMsgFromClient " << method;
|
||||
if (method == "clientReady") {
|
||||
std::string savedConfig;
|
||||
savedConfig = json11::Json(cfgOptions).dump();
|
||||
sendToClient("agentReady", savedConfig);
|
||||
} else if (method == "getConfiguration") {
|
||||
dbg_log() << "Webserver: " << wserv.hostname;
|
||||
if (wserv.hostname == "none") {
|
||||
settings_result = getDefaultConfig();
|
||||
sendToClient("configuration", settings_result);
|
||||
} else {
|
||||
std::map<std::string, std::string> attrs;
|
||||
std::string hashkey = !force_key.empty() ? force_key :
|
||||
key_store->getCookieVal("hash_key", wserv.hostname);
|
||||
#ifdef USE_GNUTLS
|
||||
if (mserv.is_tls)
|
||||
attrs["ssl"] = "1";
|
||||
#endif
|
||||
if (isValidHashkey(hashkey))
|
||||
attrs["key"] = hashkey; // attrs["nokey"] = "";
|
||||
if (!args["consent"].string_value().empty())
|
||||
attrs["consent"] = args["consent"].string_value();
|
||||
std::string url = wserv_settingsurl;
|
||||
HttpClientConnection::addUrlPars(url, attrs);
|
||||
log() << wserv.hostname << ':' << wserv.port
|
||||
<< " " << url;
|
||||
addNewTask(new SingleRequestTask(url, "msettings",
|
||||
"", wserv), this);
|
||||
}
|
||||
} else if (method == "getContent") {
|
||||
std::map<std::string, std::string> attrs;
|
||||
for (auto p : args.object_items())
|
||||
if (!p.second.string_value().empty())
|
||||
attrs[p.first] = p.second.string_value();
|
||||
std::string url = wserv_contentsurl;
|
||||
HttpClientConnection::addUrlPars(url, attrs);
|
||||
addNewTask(new SingleRequestTask(url, "contents",
|
||||
"", wserv), this);
|
||||
} else if (method == "setConfigurationOption") {
|
||||
for (auto p : args.object_items())
|
||||
handleConfigurationOption(p.first, p.second.string_value());
|
||||
|
||||
} else if (method =="saveConfigurationOption") {
|
||||
for (auto p : args.object_items()) {
|
||||
auto key = p.first, value = p.second.string_value();
|
||||
if (key == "Client.hashkey") {
|
||||
std::string url = wserv_settingsurl;
|
||||
|
||||
if (value.empty()) {
|
||||
key_store->eraseCookies(wserv.hostname);
|
||||
key_store->eraseCookies("bredbandskollen.se");
|
||||
key_store->save();
|
||||
addNewTask(new SingleRequestTask(url, "msettings",
|
||||
"", wserv), this);
|
||||
//value = createHashKey();
|
||||
} else if (isValidHashkey(value)) {
|
||||
key_store->setCookie("hash_key="+value+"; max-age=999999999"
|
||||
"; path=/; domain=.bredbandskollen.se",
|
||||
"bredbandskollen.se", "/");
|
||||
if (key_store->save())
|
||||
err_log() << "cannot save cookies";
|
||||
else
|
||||
cfgOptions.erase(key);
|
||||
|
||||
std::map<std::string, std::string> attrs;
|
||||
attrs["key"] = value;
|
||||
HttpClientConnection::addUrlPars(url, attrs);
|
||||
addNewTask(new SingleRequestTask(url, "msettings",
|
||||
"", wserv), this);
|
||||
} else {
|
||||
err_log() << "invalid Client.hashkey: " << value;
|
||||
sendToClient("setInfo", MeasurementTask::
|
||||
json_obj("keyRejected", value));
|
||||
|
||||
}
|
||||
//handleConfigurationOption(key, value);
|
||||
}
|
||||
if (value.empty()) {
|
||||
cfgOptions.erase(key);
|
||||
} else {
|
||||
cfgOptions.set(key, value);
|
||||
}
|
||||
}
|
||||
if (!options_filename.empty()) {
|
||||
if (!cfgOptions.saveJsonToFile(options_filename))
|
||||
sendToClient("setInfo", MeasurementTask::json_obj("error",
|
||||
"cannot save configuration options"));
|
||||
}
|
||||
} else if (method == "saveReport") {
|
||||
if (current_test)
|
||||
current_test->doSaveReport(args);
|
||||
} else if (method == "checkHost") {
|
||||
std::string hostname = args["server"].string_value();
|
||||
HttpHost tmpServer(wserv);
|
||||
tmpServer.hostname = hostname;
|
||||
addNewTask(new SingleRequestTask("/pingsweep/check", "checkHost",
|
||||
"", tmpServer), this);
|
||||
} else if (method == "startTest") {
|
||||
log() << "Args: " << args.dump();
|
||||
if (state == MeasurementState::IDLE) {
|
||||
std::string mserver = args["serverUrl"].string_value();
|
||||
std::string key = args["userKey"].string_value();
|
||||
#ifdef USE_GNUTLS
|
||||
mserv.is_tls = static_cast<bool>(args["tls"].int_value());
|
||||
mserv.port = mserv.is_tls ? 443 : 80;
|
||||
#else
|
||||
mserv.port = 80;
|
||||
#endif
|
||||
if (args.object_items().find("serverPort") !=
|
||||
args.object_items().end()) {
|
||||
int p = args["serverPort"].int_value();
|
||||
if (p && p > 0 && p < 65536) {
|
||||
mserv.port = static_cast<uint16_t>(p);
|
||||
log() << "measurement server port number " << p;
|
||||
} else
|
||||
err_log() << "invalid port number, will use default";
|
||||
}
|
||||
if (mserver.empty() || !isValidHashkey(key)) {
|
||||
json11::Json obj = json11::Json::object {
|
||||
{ "error",
|
||||
"startTest requires parameters serverUrl and userKey" },
|
||||
{ "errno", "X01" }
|
||||
};
|
||||
sendToClient("setInfo", obj.dump());
|
||||
state = MeasurementState::FINISHED;
|
||||
sendTaskComplete("global");
|
||||
return;
|
||||
}
|
||||
log() << "got measurement server " << mserver;
|
||||
mserv.hostname = mserver;
|
||||
report_template["host"] = wserv.hostname;
|
||||
report_template["key"] = key;
|
||||
current_test = new SpeedTest(this, mserv, report_template);
|
||||
state = MeasurementState::STARTED;
|
||||
addNewTask(current_test, this);
|
||||
} else {
|
||||
err_log() << "Must do resetTest before starting a new test";
|
||||
}
|
||||
} else if (method == "abortTest") {
|
||||
if (state == MeasurementState::STARTED) {
|
||||
if (current_test) {
|
||||
state = MeasurementState::ABORTED;
|
||||
abortTask(current_test);
|
||||
} else {
|
||||
state = MeasurementState::FINISHED;
|
||||
}
|
||||
} else {
|
||||
err_log("got abortTest when not in measurement");
|
||||
}
|
||||
} else if (method == "resetTest") {
|
||||
switch (state) {
|
||||
case MeasurementState::FINISHED:
|
||||
state = MeasurementState::IDLE;
|
||||
sendToClient("agentReady");
|
||||
mserv.hostname.clear();
|
||||
current_ticket.clear();
|
||||
break;
|
||||
case MeasurementState::IDLE:
|
||||
break;
|
||||
case MeasurementState::ABORTED:
|
||||
case MeasurementState::STARTED:
|
||||
err_log("got resetTest during measurement");
|
||||
json11::Json obj = json11::Json::object {
|
||||
{ "error", "got resetTest during measurement" },
|
||||
{ "errno", "X02" }
|
||||
};
|
||||
sendToClient("setInfo", obj.dump());
|
||||
break;
|
||||
}
|
||||
} else if (method == "setSubscription") {
|
||||
if (current_ticket.empty()) {
|
||||
err_log() << "cannot set subscription: latest measurement missing";
|
||||
return;
|
||||
}
|
||||
std::string id = args["speedId"].string_value();
|
||||
if (id.empty()) {
|
||||
long n = args["speedId"].int_value();
|
||||
if (n <= 0) {
|
||||
err_log() << "cannot set subscription: no speedId";
|
||||
return;
|
||||
}
|
||||
id = std::to_string(n);
|
||||
}
|
||||
std::string url = "/setSubscription?t=" + current_ticket + "&key=" +
|
||||
report_template["key"] + "&speed=" + id;
|
||||
addNewTask(new SingleRequestTask(url, "setSubscription",
|
||||
current_ticket, mserv), this);
|
||||
} else if (method == "listMeasurements") {
|
||||
std::map<std::string, std::string> uargs;
|
||||
uargs["key"] = !force_key.empty() ? force_key :
|
||||
key_store->getCookieVal("hash_key", wserv.hostname);
|
||||
for (auto p : args.object_items())
|
||||
uargs[p.first] = p.second.string_value();
|
||||
if (isValidHashkey(uargs["key"])) {
|
||||
std::string url = wserv_measurementsurl;
|
||||
HttpClientConnection::addUrlPars(url, uargs);
|
||||
log() << "Get URL: " << url;
|
||||
addNewTask(new SingleRequestTask(url, "list_measurements",
|
||||
"lmtask", wserv), this);
|
||||
} else {
|
||||
sendToClient("measurementList", "{\"measurements\":[]}");
|
||||
}
|
||||
} else if (method == "pingSweep") {
|
||||
addNewTask(new PingSweepTask(settings_result, wserv), this);
|
||||
} else if (method == "accumulateLog") {
|
||||
accumulateLog();
|
||||
} else if (method == "appendLog") {
|
||||
std::string l = args["logText"].string_value();
|
||||
if (!l.empty())
|
||||
appendLog(l);
|
||||
} else if (method == "sendLogToServer") {
|
||||
sendLogToServer();
|
||||
} else if (method == "terminate") {
|
||||
bridge = nullptr;
|
||||
setResult("");
|
||||
} else if (method == "quit") {
|
||||
bridge = nullptr;
|
||||
setResult("");
|
||||
} else {
|
||||
log() << "unknown command";
|
||||
}
|
||||
}
|
||||
|
||||
void MeasurementAgent::handleExecution(Task *sender, const std::string &msg) {
|
||||
if (sender == bridge) {
|
||||
pollBridge(msg);
|
||||
} else if (sender == current_test) {
|
||||
current_ticket = msg;
|
||||
dbg_log() << "current ticket: " << current_ticket;
|
||||
} else if (!bridge) {
|
||||
if (BridgeTask *bt = dynamic_cast<BridgeTask *>(sender)) {
|
||||
log() << "Bridge connected";
|
||||
bridge = bt;
|
||||
pollBridge(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MeasurementAgent::sendTaskProgress(const std::string &taskname,
|
||||
double speed, double progress) {
|
||||
// We can't use locale dependent number formating in JSON!
|
||||
std::ostringstream json;
|
||||
json.imbue(std::locale("C"));
|
||||
json << "{\"task\": \"" << taskname
|
||||
<< "\", \"result\": " << speed
|
||||
<< ", \"progress\": " << progress << "}";
|
||||
sendToClient("taskProgress", json.str());
|
||||
}
|
||||
|
||||
void MeasurementAgent::handleConfigurationOption(const std::string &name,
|
||||
const std::string &value) {
|
||||
log() << "option: " << name << " value: " << value;
|
||||
if (name == "Logging.LogToConsole") {
|
||||
if (!value.empty()) {
|
||||
Logger::setLogFile(std::cerr);
|
||||
}
|
||||
} else if (name.substr(0, 7) == "Client.") {
|
||||
std::string attr = name.substr(7);
|
||||
if (!attr.empty() && attr.size() <= 20 && std::string::npos ==
|
||||
attr.find_first_not_of("abcdefghijklmnopqrstuvwxyz")) {
|
||||
// TODO: check not reserved!
|
||||
if (attr == "hashkey")
|
||||
force_key = value;
|
||||
else
|
||||
report_template[attr] = value;
|
||||
} else {
|
||||
log() << "will ignore option " << name;
|
||||
}
|
||||
} else if (name.substr(0, 7) == "Report.") {
|
||||
std::string attr = name.substr(7);
|
||||
if (!attr.empty() && attr.size() <= 20 && std::string::npos ==
|
||||
attr.find_first_not_of("abcdefghijklmnopqrstuvwxyz") &&
|
||||
current_test)
|
||||
// TODO: check not reserved!
|
||||
current_test->addToReport(attr, value);
|
||||
else
|
||||
log() << "will ignore option " << name;
|
||||
} else if (name == "Measure.IpType") {
|
||||
wserv.iptype = (value == "ipv6") ? 6 : 4;
|
||||
std::string s = MeasurementTask::getLocalAddress();
|
||||
if (!s.empty())
|
||||
HttpClientTask::setLocalAddress(s, wserv.iptype);
|
||||
} else if (name == "Measure.Webserver") {
|
||||
wserv.hostname = value;
|
||||
} else if (name == "Measure.Server") {
|
||||
mserv.hostname = value;
|
||||
} else if (name == "Measure.LocalAddress") {
|
||||
if (!HttpClientTask::setLocalAddress(value, wserv.iptype))
|
||||
setError("cannot use local address");
|
||||
#ifdef USE_GNUTLS
|
||||
} else if (name == "Measure.TLS") {
|
||||
if (value == "1") {
|
||||
mserv.is_tls = true;
|
||||
wserv.is_tls = true;
|
||||
wserv.port = 443;
|
||||
} else {
|
||||
mserv.is_tls = false;
|
||||
wserv.is_tls = false;
|
||||
wserv.port = 80;
|
||||
}
|
||||
#endif
|
||||
} else if (name == "Measure.ProxyServerUrl") {
|
||||
wserv.proxyHost = value;
|
||||
mserv.proxyHost = value;
|
||||
} else if (name == "Measure.ProxyServerPort") {
|
||||
try {
|
||||
wserv.proxyPort = static_cast<uint16_t>(stoi(value));
|
||||
} catch (...) {
|
||||
wserv.proxyPort = 80;
|
||||
}
|
||||
mserv.proxyPort = wserv.proxyPort;
|
||||
} else if (name == "Measure.LoadDuration") {
|
||||
double duration = 10.0;
|
||||
try {
|
||||
duration = stod(value);
|
||||
} catch (...) {
|
||||
}
|
||||
if (duration < 2.0)
|
||||
duration = 2.0;
|
||||
else if (duration > 10.0)
|
||||
duration = 10.0;
|
||||
report_template["dlength"] = std::to_string(duration);
|
||||
report_template["ulength"] = std::to_string(duration);
|
||||
} else if (name == "Measure.SpeedLimit") {
|
||||
if (value.empty())
|
||||
report_template.erase("speedlimit");
|
||||
else
|
||||
report_template["speedlimit"] = value;
|
||||
} else if (name == "Measure.AutoSaveReport") {
|
||||
report_template["autosave"] = value;
|
||||
} else if (name == "Measure.SettingsUrl") {
|
||||
wserv_settingsurl = value;
|
||||
} else if (name == "Measure.ContentsUrl") {
|
||||
wserv_contentsurl = value;
|
||||
} else if (name == "Measure.MeasurementsUrl") {
|
||||
wserv_measurementsurl = value;
|
||||
} else if (name == "options_file") {
|
||||
// Ignore.
|
||||
} else {
|
||||
log() << name << ": unknown option";
|
||||
}
|
||||
}
|
||||
88
src/measurement/measurementagent.h
Normal file
88
src/measurement/measurementagent.h
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
|
||||
#include "../framework/bridgetask.h"
|
||||
#include "../json11/json11.hpp"
|
||||
#include "../http/httphost.h"
|
||||
#include "../http/cookiefile.h"
|
||||
|
||||
class SpeedTest;
|
||||
|
||||
class MeasurementAgent : public Task {
|
||||
public:
|
||||
MeasurementAgent(const TaskConfig &config, const HttpHost &webserver);
|
||||
void taskMessage(Task *task) override;
|
||||
void taskFinished(Task *task) override;
|
||||
|
||||
void handleExecution(Task *sender, const std::string &msg) override;
|
||||
void sendToClient(const std::string &method,
|
||||
const std::string &jsonobj = "{}") {
|
||||
if (bridge)
|
||||
bridge->sendMsgToClient(method, jsonobj);
|
||||
}
|
||||
void sendTaskComplete(const std::string &t, const std::string &res = "");
|
||||
void sendTaskProgress(const std::string &taskname,
|
||||
double speed, double progress);
|
||||
void accumulateLog() {
|
||||
setLogFile(accumulated_log);
|
||||
}
|
||||
void appendLog(const std::string &str) {
|
||||
accumulated_log << "\nAppend " << str.size() << "\n" << str;
|
||||
}
|
||||
void sendLogToServer();
|
||||
private:
|
||||
std::string getDefaultConfig();
|
||||
bool isValidHashkey(const std::string &key);
|
||||
void pollBridge(const std::string &msg);
|
||||
static bool isValidJson(const std::string &s) {
|
||||
std::string err;
|
||||
auto obj = json11::Json::parse(s, err);
|
||||
return err.empty();
|
||||
}
|
||||
|
||||
void handleMsgFromClient(const std::string &method,
|
||||
const json11::Json &args);
|
||||
void handleConfigurationOption(const std::string &name,
|
||||
const std::string &value);
|
||||
void uploadComplete();
|
||||
void doSaveReport();
|
||||
void resetCurrentTest();
|
||||
BridgeTask *bridge = nullptr;
|
||||
SpeedTest *current_test = nullptr;
|
||||
std::string current_ticket;
|
||||
std::ostringstream accumulated_log;
|
||||
|
||||
// Initial state is IDLE. When client says "startTest", state becomes
|
||||
// STARTED. When test is done, we send "testComplete global" to client
|
||||
// and set state to FINISHED. When client sends resetTest, state will
|
||||
// be reset to IDLE.
|
||||
// If client sends abortTest in state STARTED, state becomes ABORTED.
|
||||
enum class MeasurementState { IDLE, STARTED, FINISHED, ABORTED };
|
||||
MeasurementState state = MeasurementState::IDLE;
|
||||
|
||||
// If the client doesn't manage keys, we store them here:
|
||||
CookieManager *key_store;
|
||||
|
||||
std::string force_key;
|
||||
|
||||
// The web server and the measurement server:
|
||||
HttpHost wserv, mserv;
|
||||
|
||||
// Default value, might be modified by the client
|
||||
std::string wserv_contentsurl = "/api/content";
|
||||
std::string wserv_measurementsurl = "/api/measurements";
|
||||
std::string wserv_settingsurl = "/api/servers";
|
||||
std::string settings_result;
|
||||
|
||||
// Info to be included each time measurement result is sent:
|
||||
std::map<std::string, std::string> report_template;
|
||||
|
||||
TaskConfig cfgOptions;
|
||||
std::string options_filename;
|
||||
};
|
||||
62
src/measurement/measurementtask.cpp
Normal file
62
src/measurement/measurementtask.cpp
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#include <numeric>
|
||||
|
||||
#include "measurementtask.h"
|
||||
#include "../json11/json11.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef long ssize_t;
|
||||
#endif
|
||||
|
||||
void MeasurementTask::checkConnectionCount() {
|
||||
if (terminated())
|
||||
return;
|
||||
|
||||
if (active_connections < no_connections)
|
||||
dbg_log() << "checkConnectionCount: act=" << active_connections
|
||||
<< " want=" << no_connections
|
||||
<< " max=" << max_connections;
|
||||
|
||||
while (active_connections < no_connections && max_connections) {
|
||||
--max_connections;
|
||||
if (!createNewConnection())
|
||||
log() << "couldn't add http connection, " << max_connections
|
||||
<< " attempts remain";
|
||||
}
|
||||
|
||||
if (no_connections && !active_connections)
|
||||
connectionLost();
|
||||
}
|
||||
|
||||
void MeasurementTask::connAdded(SocketConnection *c) {
|
||||
++active_connections;
|
||||
log() << "conn added: " << c << " now have " << active_connections;
|
||||
}
|
||||
|
||||
void MeasurementTask::connRemoved(SocketConnection *c) {
|
||||
--active_connections;
|
||||
log() << "conn removed: " << c << " now have " << active_connections;
|
||||
if (!terminated()) {
|
||||
checkConnectionCount();
|
||||
}
|
||||
}
|
||||
|
||||
std::string MeasurementTask::json_obj(const std::string &attr,
|
||||
const std::string &value) {
|
||||
json11::Json obj = json11::Json::object {
|
||||
{ attr, value },
|
||||
};
|
||||
return obj.dump();
|
||||
}
|
||||
|
||||
std::string MeasurementTask::calculateLatency(std::vector<double> &samples) {
|
||||
if (samples.size() < 5)
|
||||
return std::string(); // Too few samples
|
||||
|
||||
// Keep the best 60%
|
||||
std::sort(samples.begin(), samples.end());
|
||||
ssize_t n = static_cast<ssize_t>(samples.size() * 3 / 5);
|
||||
|
||||
double latency_sum = std::accumulate(samples.cbegin(),
|
||||
samples.cbegin()+n, 0.0);
|
||||
return fValue(1000.0 * latency_sum / static_cast<double>(n));
|
||||
}
|
||||
115
src/measurement/measurementtask.h
Normal file
115
src/measurement/measurementtask.h
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#pragma once
|
||||
|
||||
#include "../http/httpclienttask.h"
|
||||
#include "../http/httphost.h"
|
||||
#include "defs.h"
|
||||
#include <clocale>
|
||||
#include <sstream>
|
||||
|
||||
class MeasurementTask : public HttpClientTask {
|
||||
public:
|
||||
MeasurementTask(const std::string &name, const std::string &ticket,
|
||||
const HttpHost &httpserver,
|
||||
unsigned int no_conn = 1, unsigned int max_conn = 3,
|
||||
double timeout = 25.0) :
|
||||
HttpClientTask(name, httpserver),
|
||||
no_connections(no_conn),
|
||||
active_connections(0),
|
||||
max_connections(max_conn),
|
||||
timeout_sec(timeout),
|
||||
ticket_string(ticket) {
|
||||
setUserAgentString(measurement::appName + " " + measurement::appVersion);
|
||||
}
|
||||
|
||||
std::string t() {
|
||||
return ticket_string;
|
||||
}
|
||||
|
||||
// Only use keep-alive within the same measurement:
|
||||
std::string cacheLabel() override {
|
||||
return ticket_string;
|
||||
}
|
||||
|
||||
// If you override this, you must call checkConnectionCount().
|
||||
// Return value is number of seconds until timerEvent
|
||||
// should be called, <= 0 for never.
|
||||
double start() override {
|
||||
checkConnectionCount();
|
||||
return timeout_sec;
|
||||
}
|
||||
|
||||
// If you override timerEvent(), you'll have to check for timeout.
|
||||
/*
|
||||
double timerEvent() override {
|
||||
if (timeout())
|
||||
setResult("");
|
||||
return 0;
|
||||
}
|
||||
*/
|
||||
|
||||
// Will be called if checkConnectionCount fails.
|
||||
// Default is to terminate task with an empty result.
|
||||
virtual void connectionLost() {
|
||||
log() << "connectionLost()";
|
||||
setResult("");
|
||||
}
|
||||
|
||||
// Returns a JSON object with a single attribute-value pair
|
||||
static std::string json_obj(const std::string &attr,
|
||||
const std::string &value);
|
||||
|
||||
// Return floating point value as string.
|
||||
// We don't want to use to_string since it's locale dependent,
|
||||
// which may break JSON.
|
||||
static std::string fValue(double x) {
|
||||
std::ostringstream s;
|
||||
s.imbue(std::locale("C"));
|
||||
s << x;
|
||||
return s.str();
|
||||
}
|
||||
|
||||
static std::string calculateLatency(std::vector<double> &samples);
|
||||
protected:
|
||||
// Tries to make sure we have at least no_connections simultaneous
|
||||
// HTTP connections. Will not try more than max_connection times.
|
||||
// Will call connectionLost on fatal failure.
|
||||
void checkConnectionCount();
|
||||
|
||||
// Will not kill already created connections.
|
||||
void setNoConnections(unsigned int no) {
|
||||
no_connections = no;
|
||||
checkConnectionCount();
|
||||
}
|
||||
|
||||
unsigned int getNoConnections() const {
|
||||
return no_connections;
|
||||
}
|
||||
|
||||
unsigned int currentNoConnections() const {
|
||||
return active_connections;
|
||||
}
|
||||
|
||||
void noMoreConnections() {
|
||||
max_connections = 0;
|
||||
}
|
||||
|
||||
bool timeout() {
|
||||
if (timeout_sec < 0)
|
||||
return false;
|
||||
if (elapsed() < timeout_sec)
|
||||
return false;
|
||||
log() << "Task timeout.";
|
||||
return true;
|
||||
}
|
||||
|
||||
double timeout_s() {
|
||||
return timeout_sec;
|
||||
}
|
||||
private:
|
||||
void connAdded(SocketConnection *) final;
|
||||
void connRemoved(SocketConnection *) final;
|
||||
|
||||
unsigned int no_connections, active_connections, max_connections;
|
||||
double timeout_sec;
|
||||
std::string ticket_string;
|
||||
};
|
||||
18
src/measurement/mk.inc
Normal file
18
src/measurement/mk.inc
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
SOURCES += \
|
||||
$(DIRLEVEL)/measurement/defs.cpp \
|
||||
$(DIRLEVEL)/measurement/speedtest.cpp \
|
||||
$(DIRLEVEL)/measurement/measurementtask.cpp \
|
||||
$(DIRLEVEL)/measurement/singlerequesttask.cpp \
|
||||
$(DIRLEVEL)/measurement/rpingtask.cpp \
|
||||
$(DIRLEVEL)/measurement/latencytask.cpp \
|
||||
$(DIRLEVEL)/measurement/pingsweeptask.cpp \
|
||||
$(DIRLEVEL)/measurement/warmuptask.cpp \
|
||||
$(DIRLEVEL)/measurement/tickettask.cpp \
|
||||
$(DIRLEVEL)/measurement/infotask.cpp \
|
||||
$(DIRLEVEL)/measurement/progresstask.cpp \
|
||||
$(DIRLEVEL)/measurement/downloadtask.cpp \
|
||||
$(DIRLEVEL)/measurement/uploadtask.cpp \
|
||||
$(DIRLEVEL)/measurement/measurementagent.cpp \
|
||||
$(DIRLEVEL)/measurement/uploadinfotask.cpp
|
||||
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
57
src/measurement/pingsweeptask.cpp
Normal file
57
src/measurement/pingsweeptask.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#include "pingsweeptask.h"
|
||||
|
||||
PingSweepTask::PingSweepTask(const std::string &cfg,
|
||||
const HttpHost &server) :
|
||||
HttpClientTask("pingsweep", server),
|
||||
config(json11::Json::parse(cfg, json_err)) {
|
||||
}
|
||||
|
||||
double PingSweepTask::start() {
|
||||
unsigned int no_calls = 0;
|
||||
for (auto srv : config["servers"].array_items()) {
|
||||
//log() << "Ping " << srv["url"].string_value();
|
||||
setServer(srv["url"].string_value());
|
||||
if (createNewConnection())
|
||||
++no_calls;
|
||||
}
|
||||
if (no_calls)
|
||||
return 5.0;
|
||||
setResult("");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double PingSweepTask::timerEvent() {
|
||||
setResult(best_host);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PingSweepTask::newRequest(HttpClientConnection *conn) {
|
||||
//log() << "newRequest " << conn->hostname();
|
||||
auto p = requests.find(conn->hostname());
|
||||
if (p != requests.end())
|
||||
return; // Ignore host since we already have made a request to it.
|
||||
|
||||
requests[conn->hostname()] = timeNow();
|
||||
conn->get("/pingsweep/req" + std::to_string(requests.size()));
|
||||
}
|
||||
|
||||
bool PingSweepTask::requestComplete(HttpClientConnection *conn) {
|
||||
//log() << "requestComplete " << conn->hostname() << " size " << requests.size();
|
||||
auto p = requests.find(conn->hostname());
|
||||
if (p != requests.end()) {
|
||||
double t = 1000 * secondsSince(p->second);
|
||||
std::string r = conn->hostname() + " " + std::to_string(t) + " ms";
|
||||
log() << r;
|
||||
if (results.empty())
|
||||
setMessage("ping info");
|
||||
results.push_back(r);
|
||||
requests.erase(p);
|
||||
if (t < best_time) {
|
||||
best_time = t;
|
||||
best_host = conn->hostname();
|
||||
}
|
||||
}
|
||||
if (requests.empty())
|
||||
setResult(best_host);
|
||||
return false;
|
||||
}
|
||||
32
src/measurement/pingsweeptask.h
Normal file
32
src/measurement/pingsweeptask.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include "../json11/json11.hpp"
|
||||
#include "../http/httpclienttask.h"
|
||||
|
||||
class PingSweepTask : public HttpClientTask {
|
||||
public:
|
||||
PingSweepTask(const std::string &obj, const HttpHost &server);
|
||||
double start() override;
|
||||
double timerEvent() override;
|
||||
void newRequest(HttpClientConnection *) override;
|
||||
bool requestComplete(HttpClientConnection *conn) override;
|
||||
std::string cacheLabel() override {
|
||||
return std::string(); // Don't store in keep-alive cache
|
||||
}
|
||||
std::string getResult() {
|
||||
if (results.empty())
|
||||
return std::string();
|
||||
std::string res = results.front();
|
||||
results.pop_front();
|
||||
return res;
|
||||
}
|
||||
private:
|
||||
std::string json_err;
|
||||
json11::Json config;
|
||||
std::map<std::string, TimePoint> requests;
|
||||
std::string best_host;
|
||||
std::deque<std::string> results;
|
||||
double best_time = 1e6;
|
||||
};
|
||||
79
src/measurement/progresstask.cpp
Normal file
79
src/measurement/progresstask.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#include "progresstask.h"
|
||||
|
||||
void ProgressTask::doTestProgress(double mbps, double duration,
|
||||
unsigned int no_conn) {
|
||||
if (duration <= current_duration || terminated())
|
||||
return;
|
||||
|
||||
current_duration = duration;
|
||||
|
||||
// Don't let the speed decrease the last half second:
|
||||
if (duration < tot_duration-0.5 || mbps > current_mbps)
|
||||
current_mbps = mbps;
|
||||
|
||||
if (duration >= tot_duration) {
|
||||
setResult(fValue(current_mbps));
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage("progress");
|
||||
log() << "task progress " << current_mbps << ' ' << duration/tot_duration;
|
||||
|
||||
if (duration > tot_duration-0.35) {
|
||||
// Less than 0.35 seconds left, don't make any new requests
|
||||
// (but let existing requests keep going).
|
||||
noMoreConnections();
|
||||
current_load_size = 0;
|
||||
soon_finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate appropriate load size for subsequent requests
|
||||
|
||||
//unsigned int n = getNoConnections();
|
||||
if (!no_conn)
|
||||
return;
|
||||
|
||||
double time_left = tot_duration - duration;
|
||||
|
||||
// We'll probably load this number of bytes during the time_left:
|
||||
double exp_bytes;
|
||||
if (speedlimit_mbps > 0.0)
|
||||
exp_bytes = std::min(speedlimit_mbps, mbps) * std::min(time_left, 0.3) / 0.000008;
|
||||
else
|
||||
exp_bytes = mbps * time_left / 0.000008;
|
||||
|
||||
current_load_size = static_cast<size_t>(exp_bytes / 4.0 / no_conn);
|
||||
if (current_load_size > 40000000)
|
||||
current_load_size = 40000000;
|
||||
else if (current_load_size < 6000)
|
||||
current_load_size = 6000;
|
||||
|
||||
if (speedlimit_mbps > mbps) {
|
||||
dbg_log() << "We're going too slow, wake up the passive connections";
|
||||
wakeUp();
|
||||
}
|
||||
}
|
||||
|
||||
size_t ProgressTask::loadSize() {
|
||||
// For very fast network, speed up even before first doTestProgress:
|
||||
if (++no_started_loads == load_size_check && current_duration<=0) {
|
||||
double time = elapsed();
|
||||
double speed = addOverheadMbps(byteCount(), time);
|
||||
doTestProgress(speed, time, currentNoConnections());
|
||||
}
|
||||
if (speedlimit_mbps > 0.0) {
|
||||
double time = elapsed();
|
||||
double speed = addOverheadMbps(byteCount(), time);
|
||||
if (speed > speedlimit_mbps) {
|
||||
log() << "going too fast, will pause";
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return current_load_size;
|
||||
}
|
||||
|
||||
bool ProgressTask::requestComplete(HttpClientConnection *) {
|
||||
return true;
|
||||
}
|
||||
108
src/measurement/progresstask.h
Normal file
108
src/measurement/progresstask.h
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
#pragma once
|
||||
|
||||
#include "measurementtask.h"
|
||||
|
||||
class ProgressTask : public MeasurementTask {
|
||||
public:
|
||||
ProgressTask(const std::string &label, const std::string &ticket_string,
|
||||
const HttpHost &server,
|
||||
unsigned int no_conn = 4, unsigned int max_conn = 20,
|
||||
double duration = 10.0, double timeout = 25.0) :
|
||||
MeasurementTask(label, ticket_string, server, no_conn, max_conn, timeout),
|
||||
byte_count(0),
|
||||
current_load_size(50000),
|
||||
load_size_check(no_conn + 2),
|
||||
current_duration(0.0),
|
||||
current_mbps(0.0) {
|
||||
if (duration < 2.0)
|
||||
tot_duration = 2.0;
|
||||
else if (duration > 20.0)
|
||||
tot_duration = 20.0;
|
||||
else
|
||||
tot_duration = duration;
|
||||
}
|
||||
|
||||
bool requestComplete(HttpClientConnection *) override;
|
||||
|
||||
void notifyBytesAndDuration(uint64_t count, double duration) {
|
||||
double mbps = addOverheadMbps(count, duration);
|
||||
doTestProgress(mbps, duration, currentNoConnections());
|
||||
}
|
||||
|
||||
void notifyBytesLoaded(size_t n) {
|
||||
byte_count += n;
|
||||
}
|
||||
|
||||
// If you want to use the threadSendCount/threadRecvCount methods, you
|
||||
// should call the below method in your start() method:
|
||||
void notifyStarted() {
|
||||
thread_send_count = SocketConnection::totBytesSent();
|
||||
thread_recv_count = SocketConnection::totBytesReceived();
|
||||
}
|
||||
|
||||
void connectionLost() override {
|
||||
// We're dead. If more than half the test was completed,
|
||||
// we keep the result.
|
||||
if (current_duration > tot_duration*0.5)
|
||||
setResult(fValue(current_mbps));
|
||||
else
|
||||
setResult("-1");
|
||||
}
|
||||
|
||||
void set_speedlimit(double limit_mbps) {
|
||||
speedlimit_mbps = (limit_mbps < 0.5) ? 0.5 : limit_mbps;
|
||||
if (speedlimit_mbps < 5.0)
|
||||
current_load_size = 5000;
|
||||
}
|
||||
|
||||
size_t loadSize();
|
||||
|
||||
double currentDuration() const {
|
||||
return current_duration;
|
||||
}
|
||||
double currentProgress() const {
|
||||
return current_duration/tot_duration;
|
||||
}
|
||||
double currentMbps() const {
|
||||
return current_mbps;
|
||||
}
|
||||
protected:
|
||||
uint64_t byteCount() {
|
||||
return byte_count;
|
||||
}
|
||||
|
||||
// Return total number of bytes sent through all sockets in the current
|
||||
// thread since last call to notifyStarted():
|
||||
uint64_t threadSendCount() {
|
||||
return SocketConnection::totBytesSent() - thread_send_count;
|
||||
}
|
||||
|
||||
// Return total number of bytes received from all sockets in the current
|
||||
// thread since last call to notifyStarted():
|
||||
uint64_t threadRecvCount() {
|
||||
return SocketConnection::totBytesReceived() - thread_recv_count;
|
||||
}
|
||||
|
||||
static double addOverheadMbps(uint64_t n, double s) {
|
||||
if (s <= 0.0)
|
||||
return 0.0;
|
||||
|
||||
double mbps = static_cast<double>(n) / s * 0.000008;
|
||||
if (mbps > 8.0)
|
||||
return (mbps * 1.02 + 0.16);
|
||||
else
|
||||
return mbps * 1.04;
|
||||
}
|
||||
void doTestProgress(double mbps, double duration, unsigned int no_conn);
|
||||
bool soonFinished() const {
|
||||
return soon_finished;
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t byte_count, thread_send_count = 0, thread_recv_count = 0;
|
||||
size_t current_load_size;
|
||||
unsigned int load_size_check, no_started_loads = 0;
|
||||
double tot_duration, current_duration, current_mbps;
|
||||
double speedlimit_mbps = 0.0;
|
||||
bool soon_finished = false;
|
||||
};
|
||||
61
src/measurement/rpingtask.cpp
Normal file
61
src/measurement/rpingtask.cpp
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <sstream>
|
||||
#include "rpingtask.h"
|
||||
|
||||
void RpingTask::newRequest(HttpClientConnection *conn) {
|
||||
conn->ws_get("/ws?t=" + t());
|
||||
}
|
||||
|
||||
bool RpingTask::wsTextMessage(HttpConnection *conn,
|
||||
const std::string &msg) {
|
||||
if (!max_roundtrips) {
|
||||
setResult("");
|
||||
return false;
|
||||
}
|
||||
--max_roundtrips;
|
||||
|
||||
std::istringstream line(msg);
|
||||
std::string cmd, challenge;
|
||||
double val;
|
||||
line >> cmd;
|
||||
if (sent_challenge) {
|
||||
line >> val;
|
||||
if (cmd == "latency_result") {
|
||||
setResult(fValue(val));
|
||||
return false;
|
||||
} else if (line && val > 0.0) {
|
||||
samples.push_back(val);
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd != "challenge") {
|
||||
err_log() << "unknown message: " << msg;
|
||||
return false;
|
||||
}
|
||||
log() << "Received " << msg;
|
||||
|
||||
line >> challenge;
|
||||
if (!line || challenge.size() > 10) {
|
||||
err_log() << "Bad rping challenge";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (samples.size() >= 12) {
|
||||
conn->sendWsMessage("rping end");
|
||||
log() << "Sending rping end";
|
||||
return true;
|
||||
}
|
||||
|
||||
conn->sendWsMessage("rping " + challenge);
|
||||
log() << "Sending rping " << challenge;
|
||||
sent_challenge = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RpingTask::websocketUpgrade(HttpClientConnection *conn) {
|
||||
conn->sendWsMessage("rping start");
|
||||
log() << "Sending rping start";
|
||||
return true;
|
||||
}
|
||||
30
src/measurement/rpingtask.h
Normal file
30
src/measurement/rpingtask.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "measurementtask.h"
|
||||
|
||||
class RpingTask : public MeasurementTask {
|
||||
public:
|
||||
RpingTask(const std::string &label, const std::string &ticket_string,
|
||||
const HttpHost &server,
|
||||
unsigned int no_conn = 1, unsigned int max_conn = 20,
|
||||
double duration = 25.0) :
|
||||
MeasurementTask(label, ticket_string, server,
|
||||
no_conn, max_conn, duration)
|
||||
{
|
||||
}
|
||||
void newRequest(HttpClientConnection *) override;
|
||||
bool requestComplete(HttpClientConnection *) override {
|
||||
return false;
|
||||
}
|
||||
bool websocketUpgrade(HttpClientConnection *) override;
|
||||
bool wsTextMessage(HttpConnection *conn,
|
||||
const std::string &msg) override;
|
||||
void calc_local_result();
|
||||
private:
|
||||
std::vector<double> samples;
|
||||
unsigned int max_roundtrips = 100;
|
||||
bool sent_challenge = false;
|
||||
};
|
||||
19
src/measurement/singlerequesttask.cpp
Normal file
19
src/measurement/singlerequesttask.cpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#include "singlerequesttask.h"
|
||||
|
||||
SingleRequestTask::SingleRequestTask(const std::string &url,
|
||||
const std::string &name,
|
||||
const std::string &ticket,
|
||||
const HttpHost &server) :
|
||||
MeasurementTask(name, ticket, server),
|
||||
_url(url) {
|
||||
}
|
||||
|
||||
void SingleRequestTask::newRequest(HttpClientConnection *conn) {
|
||||
conn->get(_url);
|
||||
}
|
||||
|
||||
bool SingleRequestTask::requestComplete(HttpClientConnection *conn) {
|
||||
if (conn->httpStatus() == 200)
|
||||
setResult(conn->contents());
|
||||
return false;
|
||||
}
|
||||
13
src/measurement/singlerequesttask.h
Normal file
13
src/measurement/singlerequesttask.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "measurementtask.h"
|
||||
|
||||
class SingleRequestTask : public MeasurementTask {
|
||||
public:
|
||||
SingleRequestTask(const std::string &url, const std::string &name,
|
||||
const std::string &ticket, const HttpHost &server);
|
||||
void newRequest(HttpClientConnection *conn) override;
|
||||
bool requestComplete(HttpClientConnection *conn) override;
|
||||
private:
|
||||
std::string _url;
|
||||
};
|
||||
358
src/measurement/speedtest.cpp
Normal file
358
src/measurement/speedtest.cpp
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <iomanip>
|
||||
|
||||
#include "../http/sha1.h"
|
||||
|
||||
#include "speedtest.h"
|
||||
|
||||
#include "measurementagent.h"
|
||||
#include "rpingtask.h"
|
||||
// Alternative latency measurement:
|
||||
#include "latencytask.h"
|
||||
#include "warmuptask.h"
|
||||
#include "tickettask.h"
|
||||
#include "infotask.h"
|
||||
#include "downloadtask.h"
|
||||
// Alternative download measurement:
|
||||
#include "wsdownloadtask.h"
|
||||
#include "uploadtask.h"
|
||||
// Alternative upload measurement: #include "wsuploadtask.h"
|
||||
// Alternative to some parts of infotask:
|
||||
#include "uploadinfotask.h"
|
||||
|
||||
SpeedTest::SpeedTest(MeasurementAgent *agent, const HttpHost &mserver,
|
||||
const std::map<std::string, std::string> &report_data) :
|
||||
Task("SpeedTest"),
|
||||
the_agent(agent),
|
||||
mserv(mserver),
|
||||
report(report_data)
|
||||
{
|
||||
bytesSentAtStart = SocketConnection::totBytesSent();
|
||||
bytesRecAtStart = SocketConnection::totBytesReceived();
|
||||
killChildTaskWhenFinished();
|
||||
upload_duration = std::stod(report["ulength"]);
|
||||
download_duration = std::stod(report["dlength"]);
|
||||
try {
|
||||
auto p = report.find("speedlimit");
|
||||
if (p != report.end()) {
|
||||
log() << "speedlimit " << p->second;
|
||||
speed_limit = std::stod(p->second);
|
||||
if (speed_limit < 0.5)
|
||||
speed_limit = 0.5;
|
||||
if (speed_limit <= 10.0) {
|
||||
initial_no_dconn = 2;
|
||||
max_no_dconn = 10;
|
||||
initial_no_uconn = 2;
|
||||
max_no_uconn = 4;
|
||||
}
|
||||
} else {
|
||||
log() << "no speedlimit";
|
||||
}
|
||||
} catch (...) {
|
||||
the_agent->sendToClient("setInfo", MeasurementTask::
|
||||
json_obj("warning", "cannot parse SpeedLimit value"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
double SpeedTest::start() {
|
||||
startObserving(the_agent);
|
||||
addNewTask(new TicketTask(mserv, report["key"], report["host"]), this);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void SpeedTest::taskMessage(Task *task) {
|
||||
if (!the_agent)
|
||||
return;
|
||||
|
||||
std::string name = task->label(),
|
||||
message = task->message();
|
||||
|
||||
if (task == info_task) {
|
||||
log() << "GOT INFO: " << message;
|
||||
if (message == "server upload timeout") {
|
||||
uploadComplete();
|
||||
return;
|
||||
}
|
||||
std::string err;
|
||||
auto obj = json11::Json::parse(message, err);
|
||||
if (!err.empty()) {
|
||||
err_log() << "JSON error, ignoring message";
|
||||
return;
|
||||
}
|
||||
std::string event = obj["event"].string_value();
|
||||
const json11::Json &args = obj["args"];
|
||||
// {"event": "measurementInfo",
|
||||
// "args": {
|
||||
// "ispname":"Telia","date":"Thu, 30 Nov 2017 15:25:31 +0000",
|
||||
// "rating":"GOOD","MeasurementID":"109"
|
||||
// }
|
||||
// }
|
||||
if (event == "measurementInfo") {
|
||||
the_agent->sendToClient("measurementInfo", args.dump());
|
||||
setResult("");
|
||||
} else if (event == "uploadInfo") {
|
||||
/* {
|
||||
"event": "uploadInfo",
|
||||
"args": {
|
||||
"duration": 1.506616427,
|
||||
"speed": 37348.559901815221
|
||||
}
|
||||
}
|
||||
*/
|
||||
double speed = args["speed"].number_value();
|
||||
double duration = args["duration"].number_value();
|
||||
|
||||
log() << "uploadInfo Duration: " << duration
|
||||
<< " Speed: " << speed;
|
||||
if (duration >= upload_duration) {
|
||||
server_upload_speed = MeasurementTask::fValue(speed);
|
||||
uploadComplete();
|
||||
} else if (duration > 0)
|
||||
the_agent->sendTaskProgress("upload", speed,
|
||||
duration/upload_duration);
|
||||
} else {
|
||||
log() << "unknown event: " << event;
|
||||
}
|
||||
} else if (ProgressTask *ptask = dynamic_cast<ProgressTask *>(task)) {
|
||||
double speed = ptask->currentMbps();
|
||||
double progress = ptask->currentProgress();
|
||||
if (name != "upload")
|
||||
the_agent->sendTaskProgress(name, speed, progress);
|
||||
log() << name << " speed " << speed << " Mbit/s, progress " << progress;
|
||||
} else {
|
||||
log() << name << " message: " << message;
|
||||
}
|
||||
}
|
||||
|
||||
void SpeedTest::taskFinished(Task *task) {
|
||||
std::string name = task->label(),
|
||||
result = task->result();
|
||||
|
||||
if (task->wasKilled())
|
||||
log() << "Task " << name << " killed";
|
||||
else
|
||||
log() << "Task " << name << " finished, result: " << result;
|
||||
|
||||
if (task == the_agent) {
|
||||
the_agent = nullptr;
|
||||
log() << "Agent gone, will exit";
|
||||
setResult("");
|
||||
} else if (task == info_task) {
|
||||
info_task = nullptr;
|
||||
if (report_sent_to_server)
|
||||
setResult("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!the_agent)
|
||||
return;
|
||||
|
||||
if (name == "ticket") {
|
||||
if (result.empty()) {
|
||||
json11::Json obj = json11::Json::object {
|
||||
{ "error", "no network access to server" },
|
||||
{ "errno", "C01" }
|
||||
};
|
||||
the_agent->sendToClient("setInfo", obj.dump());
|
||||
setResult("");
|
||||
} else {
|
||||
tstr = result;
|
||||
if (TicketTask *ttask = dynamic_cast<TicketTask *>(task))
|
||||
report["localip"] = ttask->localIp();
|
||||
|
||||
the_agent->sendToClient("setInfo", MeasurementTask::
|
||||
json_obj("ticket", tstr));
|
||||
the_agent->sendToClient("taskStart", MeasurementTask::
|
||||
json_obj("task", "latency"));
|
||||
info_task = new InfoTask("measurement info", tstr,
|
||||
report["key"], mserv);
|
||||
|
||||
addNewTask(info_task, this);
|
||||
RpingTask *t = new RpingTask("rping", tstr, mserv);
|
||||
addNewTask(t, this);
|
||||
|
||||
// Send the ticket to the agent. The agent may use it to update
|
||||
// subscription into or fetch logs after the measurement is done.
|
||||
executeHandler(the_agent, tstr);
|
||||
}
|
||||
} else if (name == "measurementStart") {
|
||||
std::string err;
|
||||
auto obj = json11::Json::parse(result, err);
|
||||
if (!result.empty() && err.empty()) {
|
||||
std::string ip = obj["ip"].string_value();
|
||||
if (!ip.empty())
|
||||
log() << "Client external ip number: " << ip;
|
||||
if (local_latency) {
|
||||
double latency = obj["latency"].number_value();
|
||||
if (latency > 0) {
|
||||
local_latency = false;
|
||||
report["latency"] = std::to_string(latency);
|
||||
log() << "Adjusted latency: " << latency;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (name == "rping" || name == "httplatency") {
|
||||
if (name == "rping") {
|
||||
if (result.size()) {
|
||||
websocket_works = true;
|
||||
} else {
|
||||
addNewTask(new LatencyTask(tstr, mserv), this);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
local_latency = true;
|
||||
if (!result.size())
|
||||
result = "-1";
|
||||
}
|
||||
report["latency"] = result;
|
||||
the_agent->sendTaskComplete("latency", result);
|
||||
std::string url = "/measurementStarted?t=" + tstr;
|
||||
//url += "&uptick=120";
|
||||
|
||||
HttpClientConnection::addUrlPars(url, report);
|
||||
log() << "measurementStart: " << mserv.hostname << url;
|
||||
addNewTask(new SingleRequestTask(url, "measurementStart",
|
||||
tstr, mserv), this);
|
||||
addNewTask(new WarmUpTask(tstr, mserv, initial_no_dconn, 3.0),
|
||||
this);
|
||||
} else if (name == "download") {
|
||||
if (!result.size())
|
||||
result = "-1";
|
||||
the_agent->sendTaskComplete("download", result);
|
||||
report["download"] = result;
|
||||
the_agent->sendToClient("taskStart", MeasurementTask::
|
||||
json_obj("task", "upload"));
|
||||
// UploadInfoTask fetches upload speed/progress from the server.
|
||||
// If the final speed sill hasn't arrived 3.0 seconds after the end of
|
||||
// the upload measurement, we give up and use the local upload speed
|
||||
// estimate which is lower since we can only count confirmed data.
|
||||
if (!websocket_works)
|
||||
addNewTask(new UploadInfoTask(tstr, mserv, upload_duration,
|
||||
upload_duration+3.0), this);
|
||||
UploadTask *t = new UploadTask(tstr, mserv, initial_no_uconn,
|
||||
max_no_uconn, upload_duration);
|
||||
if (speed_limit > 0)
|
||||
t->set_speedlimit(speed_limit);
|
||||
addNewTask(t, this);
|
||||
} else if (name == "saveReport") {
|
||||
uint64_t totBytesSent = SocketConnection::totBytesSent() - bytesSentAtStart;
|
||||
uint64_t totBytesReceived = SocketConnection::totBytesReceived() - bytesRecAtStart;
|
||||
|
||||
json11::Json bytesInfo = json11::Json::object {
|
||||
{ "totBytesReceived", std::to_string(totBytesReceived)},
|
||||
{ "totBytesSent", std::to_string(totBytesSent) }
|
||||
};
|
||||
|
||||
the_agent->sendToClient("setInfo", bytesInfo.dump());
|
||||
|
||||
std::string err;
|
||||
auto obj = json11::Json::parse(result, err);
|
||||
if (!result.empty() && err.empty()) {
|
||||
the_agent->sendToClient("report", result);
|
||||
if (report["host"] == "none") {
|
||||
// Local measurement, no more info will arrive.
|
||||
setResult("");
|
||||
return;
|
||||
}
|
||||
log() << "INFO: " << result;
|
||||
// Wait at most 3 seconds for final info from server.
|
||||
if (info_task) {
|
||||
info_task->setInfoDeadline(3.0);
|
||||
info_task->resetTimer(0.1);
|
||||
} else {
|
||||
std::string url = "/getUpdate?t=" + tstr +
|
||||
"&key=" + report["key"];
|
||||
addNewTask(new SingleRequestTask(url, "getUpdate", tstr,
|
||||
mserv), this);
|
||||
}
|
||||
} else {
|
||||
the_agent->sendToClient("report", "{}");
|
||||
log() << "invalid saveReport: " << result;
|
||||
setResult("");
|
||||
}
|
||||
} else if (name == "warmup") {
|
||||
the_agent->sendToClient("taskStart", MeasurementTask::
|
||||
json_obj("task", "download"));
|
||||
//WsDownloadTask *t = new WsDownloadTask(tstr, mserv, initial_no_dconn,
|
||||
DownloadTask *t = new DownloadTask(tstr, mserv, initial_no_dconn,
|
||||
max_no_dconn, download_duration);
|
||||
log() << "t->set_speedlimit " << speed_limit;
|
||||
if (speed_limit > 0)
|
||||
t->set_speedlimit(speed_limit);
|
||||
addNewTask(t, this);
|
||||
} else if (name == "upload") {
|
||||
local_upload_speed = result.size() ? result : "0";
|
||||
if (server_upload_speed.empty()) {
|
||||
if (info_task) {
|
||||
// Wait for up to 3 seconds for server upload speed:
|
||||
info_task->setUploadDeadline(3.0);
|
||||
info_task->resetTimer(0.1);
|
||||
} else {
|
||||
// Pointless to wait for server upload info.
|
||||
uploadComplete();
|
||||
}
|
||||
}
|
||||
} else if (name == "getUpdate") {
|
||||
std::string err;
|
||||
auto obj = json11::Json::parse(result, err);
|
||||
std::string event = obj["event"].string_value();
|
||||
if (err.empty() && event == "measurementInfo") {
|
||||
const json11::Json &args = obj["args"];
|
||||
the_agent->sendToClient("measurementInfo", args.dump());
|
||||
|
||||
} else {
|
||||
err_log() << "JSON error, ignoring message";
|
||||
the_agent->sendToClient("measurementInfo", "{}");
|
||||
}
|
||||
setResult("");
|
||||
} else {
|
||||
log() << "unknown task, ignoring";
|
||||
}
|
||||
}
|
||||
|
||||
void SpeedTest::uploadComplete() {
|
||||
if (report.find("upload") != report.end()) {
|
||||
log() << "Upload already set: " << report["upload"];
|
||||
return;
|
||||
}
|
||||
|
||||
// Local and server speed calculations are complete.
|
||||
// Use local calculation only if server calculation failed:
|
||||
std::string result = server_upload_speed;
|
||||
if (result.empty() || (result == "0" && !local_upload_speed.empty()))
|
||||
result = local_upload_speed;
|
||||
report["upload"] = result;
|
||||
the_agent->sendTaskComplete("upload", result);
|
||||
auto p = report.find("autosave");
|
||||
if (p == report.end() || p->second != "false")
|
||||
doSaveReport();
|
||||
else
|
||||
log() << "Wait for client to be ready for saving report";
|
||||
if (info_task)
|
||||
info_task->setUploadDeadline(-1.0);
|
||||
}
|
||||
|
||||
void SpeedTest::addToReport(const std::string &attr, const std::string &val) {
|
||||
report[attr] = val;
|
||||
}
|
||||
|
||||
void SpeedTest::doSaveReport(const json11::Json &args) {
|
||||
if (report_sent_to_server)
|
||||
return;
|
||||
report_sent_to_server = true;
|
||||
std::string url("/saveReport?t=" + tstr);
|
||||
|
||||
for (auto p : args.object_items()) {
|
||||
const std::string &attr = p.first;
|
||||
if (report.find(attr) == report.end())
|
||||
report[attr] = p.second.string_value();
|
||||
}
|
||||
HttpClientConnection::addUrlPars(url, report);
|
||||
|
||||
log() << "saveReport " << url;
|
||||
addNewTask(new SingleRequestTask(url, "saveReport",
|
||||
tstr, mserv), this);
|
||||
}
|
||||
48
src/measurement/speedtest.h
Normal file
48
src/measurement/speedtest.h
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
// Perform a standard speed test, measuring latency, download speed, and upload
|
||||
// speed.
|
||||
|
||||
#include "../json11/json11.hpp"
|
||||
#include "../framework/task.h"
|
||||
#include "../http/httphost.h"
|
||||
|
||||
class MeasurementAgent;
|
||||
class InfoTask;
|
||||
|
||||
class SpeedTest : public Task {
|
||||
public:
|
||||
SpeedTest(MeasurementAgent *agent, const HttpHost &mserver,
|
||||
const std::map<std::string, std::string> &report_data);
|
||||
double start() override;
|
||||
void taskMessage(Task *task) override;
|
||||
void taskFinished(Task *task) override;
|
||||
void uploadComplete();
|
||||
void doSaveReport(const json11::Json &args = json11::Json::object());
|
||||
void addToReport(const std::string &attr, const std::string &val);
|
||||
private:
|
||||
MeasurementAgent *the_agent = nullptr;
|
||||
InfoTask *info_task = nullptr;
|
||||
HttpHost mserv;
|
||||
std::map<std::string, std::string> report;
|
||||
std::string tstr;
|
||||
double upload_duration, download_duration;
|
||||
std::string server_upload_speed, local_upload_speed;
|
||||
|
||||
bool report_sent_to_server = false;
|
||||
bool websocket_works = false;
|
||||
// Normally, the server should calculate the latency result.
|
||||
// If that failed and we have a locally calculated latency result,
|
||||
// this will be set to true:
|
||||
bool local_latency = false;
|
||||
|
||||
double speed_limit = 0.0;
|
||||
unsigned int initial_no_dconn = 10, initial_no_uconn = 4;
|
||||
unsigned int max_no_dconn = 100, max_no_uconn = 100;
|
||||
|
||||
uint64_t bytesSentAtStart;
|
||||
uint64_t bytesRecAtStart;
|
||||
};
|
||||
28
src/measurement/tickettask.cpp
Normal file
28
src/measurement/tickettask.cpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#include "../json11/json11.hpp"
|
||||
#include "tickettask.h"
|
||||
|
||||
TicketTask::TicketTask(const HttpHost &server, const std::string &key,
|
||||
const std::string &host) :
|
||||
SingleRequestTask("/ticket?key=" + key + "&host=" + host, "ticket", "",
|
||||
server) {
|
||||
}
|
||||
|
||||
bool TicketTask::requestComplete(HttpClientConnection *conn) {
|
||||
if (conn->httpStatus() == 200) {
|
||||
std::string err;
|
||||
json11::Json ticket_obj = json11::Json::parse(conn->contents(), err);
|
||||
if (err.empty()) {
|
||||
_ticket = ticket_obj["ticket"].string_value();
|
||||
if (!_ticket.empty()) {
|
||||
_localIp = conn->localIp();
|
||||
setResult(_ticket);
|
||||
} else {
|
||||
err_log() << "no ticket found";
|
||||
}
|
||||
} else {
|
||||
err_log() << "cannot parse ticket: " << err;
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
20
src/measurement/tickettask.h
Normal file
20
src/measurement/tickettask.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include "singlerequesttask.h"
|
||||
|
||||
class TicketTask : public SingleRequestTask {
|
||||
public:
|
||||
TicketTask(const HttpHost &server, const std::string &key,
|
||||
const std::string &host);
|
||||
|
||||
std::string cacheLabel() override {
|
||||
return _ticket;
|
||||
}
|
||||
|
||||
bool requestComplete(HttpClientConnection *conn) override;
|
||||
const std::string &localIp() const {
|
||||
return _localIp;
|
||||
}
|
||||
private:
|
||||
std::string _ticket, _localIp;
|
||||
};
|
||||
41
src/measurement/uploadinfotask.cpp
Normal file
41
src/measurement/uploadinfotask.cpp
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#include "uploadinfotask.h"
|
||||
|
||||
UploadInfoTask::
|
||||
UploadInfoTask(const std::string &ticket, const HttpHost &server,
|
||||
double duration, double max_time) :
|
||||
ProgressTask("uploadinfo", ticket, server, 1, 3, duration, max_time) {
|
||||
}
|
||||
|
||||
double UploadInfoTask::start() {
|
||||
checkConnectionCount();
|
||||
return timeout_s();
|
||||
}
|
||||
|
||||
void UploadInfoTask::newRequest(HttpClientConnection *conn) {
|
||||
conn->get("/ulinfo/1.txt?t=" + t());
|
||||
}
|
||||
|
||||
|
||||
bool UploadInfoTask::headerComplete(HttpClientConnection *conn) {
|
||||
conn->doStreamResponse();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UploadInfoTask::payload(HttpClientConnection *, char *buf, size_t len) {
|
||||
buffer.append(buf, len);
|
||||
while (true) {
|
||||
std::string::size_type pos = buffer.find("\r\n");
|
||||
if (pos == std::string::npos)
|
||||
return;
|
||||
std::istringstream line(buffer.substr(0, pos));
|
||||
uint64_t byte_count;
|
||||
double duration;
|
||||
if (line >> byte_count >> duration) {
|
||||
log() << "server upload info " << byte_count << ' ' << duration;
|
||||
notifyBytesAndDuration(byte_count, duration);
|
||||
} else if (pos) {
|
||||
err_log() << "bad server info line: " << buffer.substr(0, pos);
|
||||
}
|
||||
buffer.erase(0, pos+2);
|
||||
}
|
||||
}
|
||||
15
src/measurement/uploadinfotask.h
Normal file
15
src/measurement/uploadinfotask.h
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#pragma once
|
||||
|
||||
#include "progresstask.h"
|
||||
|
||||
class UploadInfoTask : public ProgressTask {
|
||||
public:
|
||||
UploadInfoTask(const std::string &ticket, const HttpHost &server,
|
||||
double duration = 10.0, double max_time = 25.0);
|
||||
double start() override;
|
||||
void newRequest(HttpClientConnection *) override;
|
||||
bool headerComplete(HttpClientConnection *) override;
|
||||
void payload(HttpClientConnection *, char *, size_t ) override;
|
||||
private:
|
||||
std::string buffer;
|
||||
};
|
||||
83
src/measurement/uploadtask.cpp
Normal file
83
src/measurement/uploadtask.cpp
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "uploadtask.h"
|
||||
#include <vector>
|
||||
|
||||
UploadTask::UploadTask(const std::string &ticket, const HttpHost &server,
|
||||
unsigned int no_conn, unsigned int max_conn,
|
||||
double duration, double max_time, double tick_s) :
|
||||
ProgressTask("upload", ticket, server, no_conn, max_conn, duration, max_time),
|
||||
tick_duration_s(tick_s>0 ? tick_s : 0.1) {
|
||||
}
|
||||
|
||||
double UploadTask::start() {
|
||||
log() << "UploadTask starting";
|
||||
notifyStarted();
|
||||
checkConnectionCount();
|
||||
return tick_duration_s;
|
||||
}
|
||||
|
||||
void UploadTask::newRequest(HttpClientConnection *conn) {
|
||||
if (size_t to_write = loadSize()) {
|
||||
conn->post("/cgi/upload.cgi?t=" + t() + "&id=1", to_write);
|
||||
post_size[conn] = to_write;
|
||||
} else {
|
||||
conn->pass();
|
||||
}
|
||||
}
|
||||
|
||||
bool UploadTask::requestComplete(HttpClientConnection *conn) {
|
||||
if (conn->httpStatus() == 200) {
|
||||
// Now we know for sure that all posted data has arrived at the server
|
||||
notifyBytesLoaded(post_size[conn]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double UploadTask::timerEvent() {
|
||||
double time = elapsed();
|
||||
|
||||
if (time >= timeout_s()) {
|
||||
log() << "UploadTask timeout after " << time << " seconds";
|
||||
setResult("-1");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (time > 0.05) {
|
||||
// log() << "Check: " << byteCount() << " and " << threadSendCount();
|
||||
|
||||
// Note: byteCount() is the number of bytes we know for sure have
|
||||
// arrived at the server, threadSendCount() is the number of bytes
|
||||
// we've put into socket buffers. We have to be careful and use the
|
||||
// lower value, i.e. byteCount(), which is too low, instead of the
|
||||
// higher value, i.e. threadSendCount(), which is too large.
|
||||
// However, the server knows the correct value and will send it to
|
||||
// us regularly through the InfoTask.
|
||||
|
||||
double mbps = addOverheadMbps(byteCount(), time);
|
||||
doTestProgress(mbps, time, currentNoConnections());
|
||||
}
|
||||
|
||||
return tick_duration_s;
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::vector<char> getUploadBuffer(size_t len) {
|
||||
std::vector<char> buf;
|
||||
// Fill the buffer with dummy data that cannot cause data compression
|
||||
while (len) {
|
||||
buf.push_back(static_cast<char>(rand()));
|
||||
--len;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
size_t UploadTask::doPost(HttpClientConnection *conn, size_t len) {
|
||||
static std::vector<char> buffer = getUploadBuffer(post_buffer_len);
|
||||
size_t to_write = std::min(len, post_buffer_len);
|
||||
return conn->sendData(buffer.data(), to_write);
|
||||
}
|
||||
|
||||
const size_t UploadTask::post_buffer_len;
|
||||
20
src/measurement/uploadtask.h
Normal file
20
src/measurement/uploadtask.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include "progresstask.h"
|
||||
|
||||
class UploadTask : public ProgressTask {
|
||||
public:
|
||||
UploadTask(const std::string &ticket, const HttpHost &server,
|
||||
unsigned int no_conn = 4, unsigned int max_conn = 20,
|
||||
double duration = 10.0, double max_time = 25.0,
|
||||
double tick_s = 0.1);
|
||||
double start() override;
|
||||
double timerEvent() override;
|
||||
void newRequest(HttpClientConnection *conn) override;
|
||||
bool requestComplete(HttpClientConnection *conn) override;
|
||||
size_t doPost(HttpClientConnection *conn, size_t len) override;
|
||||
private:
|
||||
double tick_duration_s;
|
||||
static const size_t post_buffer_len = 131072;
|
||||
std::map<HttpClientConnection *, size_t> post_size;
|
||||
};
|
||||
9
src/measurement/warmuptask.cpp
Normal file
9
src/measurement/warmuptask.cpp
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#include "warmuptask.h"
|
||||
|
||||
void WarmUpTask::newRequest(HttpClientConnection *conn) {
|
||||
conn->get(url);
|
||||
}
|
||||
|
||||
bool WarmUpTask::requestComplete(HttpClientConnection *) {
|
||||
return false;
|
||||
}
|
||||
16
src/measurement/warmuptask.h
Normal file
16
src/measurement/warmuptask.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include "measurementtask.h"
|
||||
|
||||
class WarmUpTask : public MeasurementTask {
|
||||
public:
|
||||
WarmUpTask(const std::string &ticket, const HttpHost &server,
|
||||
unsigned int no_conn, double timeout) :
|
||||
MeasurementTask("warmup", ticket, server, no_conn, no_conn, timeout),
|
||||
url("/pingpong/warmup?t=" + ticket) {
|
||||
}
|
||||
void newRequest(HttpClientConnection *conn) override;
|
||||
bool requestComplete(HttpClientConnection *conn) override;
|
||||
private:
|
||||
std::string url;
|
||||
};
|
||||
97
src/measurement/wsdownloadtask.cpp
Normal file
97
src/measurement/wsdownloadtask.cpp
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "wsdownloadtask.h"
|
||||
|
||||
WsDownloadTask::
|
||||
WsDownloadTask(const std::string &ticket, const HttpHost &server,
|
||||
unsigned int no_conn, unsigned int max_conn,
|
||||
double duration, double max_time,
|
||||
double tick_s) :
|
||||
ProgressTask("download", ticket, server, no_conn, max_conn, duration, max_time),
|
||||
tick_duration_s(tick_s>0 ? tick_s : 0.1) {
|
||||
dynamic_conn_limit = 0.5 * duration + 0.5;
|
||||
}
|
||||
|
||||
double WsDownloadTask::start() {
|
||||
log() << "WsDownloadTask starting";
|
||||
checkConnectionCount();
|
||||
return tick_duration_s;
|
||||
}
|
||||
|
||||
double WsDownloadTask::timerEvent() {
|
||||
double time = elapsed();
|
||||
|
||||
if (time >= timeout_s()) {
|
||||
log() << "WsDownloadTask timeout after " << time << " seconds";
|
||||
setResult("-1");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (time > 0.05) {
|
||||
double speed = addOverheadMbps(byteCount(), time);
|
||||
if (time < dynamic_conn_limit && speed >= 50.0) {
|
||||
unsigned int no_conn = (speed < 250.0) ?
|
||||
((speed < 100) ? 12 : 24) :
|
||||
((speed < 500) ? 32 : 48);
|
||||
if (no_conn < currentNoConnections())
|
||||
no_conn = currentNoConnections();
|
||||
doTestProgress(speed, time, no_conn);
|
||||
setNoConnections(no_conn);
|
||||
} else {
|
||||
doTestProgress(speed, time, currentNoConnections());
|
||||
}
|
||||
}
|
||||
return tick_duration_s;
|
||||
}
|
||||
|
||||
void WsDownloadTask::newRequest(HttpClientConnection *conn) {
|
||||
conn->ws_get("/ws?t=" + t());
|
||||
//conn->dbgOn();
|
||||
}
|
||||
|
||||
#include <unistd.h>
|
||||
bool WsDownloadTask::nextRequest(HttpConnection *conn) {
|
||||
if (size_t size = loadSize()) {
|
||||
conn->sendWsMessage("download " + std::to_string(size));
|
||||
//log() << "nextRequest " << size;
|
||||
return true;
|
||||
} else if (soonFinished()) {
|
||||
log() << "nextRequest no more";
|
||||
return false;
|
||||
} else {
|
||||
log() << "nextRequest idle";
|
||||
// Keep the connection but don't make a new request at this point.
|
||||
// Perhaps due to a speed limit.
|
||||
//conn->pass();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool WsDownloadTask::requestComplete(HttpClientConnection *) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WsDownloadTask::websocketUpgrade(HttpClientConnection *conn) {
|
||||
return nextRequest(conn);
|
||||
}
|
||||
|
||||
bool WsDownloadTask::wsBinMessage(HttpConnection *conn,
|
||||
const std::string &msg) {
|
||||
log() << "Got BIN: " << msg.size();
|
||||
notifyBytesLoaded(msg.size());
|
||||
return nextRequest(conn);
|
||||
}
|
||||
|
||||
bool WsDownloadTask::wsBinData(HttpConnection *conn, const char *, size_t count) {
|
||||
notifyBytesLoaded(count);
|
||||
if (conn->wsIncomingBytesLeft())
|
||||
return true;
|
||||
return nextRequest(conn);
|
||||
}
|
||||
|
||||
bool WsDownloadTask::wsTextMessage(HttpConnection *,
|
||||
const std::string &msg) {
|
||||
log() << "Unexpected ws text message: " << msg;
|
||||
return false;
|
||||
}
|
||||
32
src/measurement/wsdownloadtask.h
Normal file
32
src/measurement/wsdownloadtask.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "progresstask.h"
|
||||
|
||||
class WsDownloadTask : public ProgressTask {
|
||||
public:
|
||||
WsDownloadTask(const std::string &ticket, const HttpHost &server,
|
||||
unsigned int no_conn = 10, unsigned int max_conn = 100,
|
||||
double duration = 10.0, double max_time = 25.0,
|
||||
double tick_s = 0.1);
|
||||
double start() override;
|
||||
double timerEvent() override;
|
||||
void newRequest(HttpClientConnection *) override;
|
||||
bool requestComplete(HttpClientConnection *) override;
|
||||
bool websocketUpgrade(HttpClientConnection *) override;
|
||||
bool wsBinMessage(HttpConnection *conn,
|
||||
const std::string &msg) override;
|
||||
bool wsTextMessage(HttpConnection *conn,
|
||||
const std::string &msg) override;
|
||||
bool wsBinHeader(HttpConnection *conn, size_t ) override {
|
||||
conn->streamWsResponse();
|
||||
return true;
|
||||
}
|
||||
bool wsBinData(HttpConnection *conn, const char *, size_t count) override;
|
||||
private:
|
||||
bool nextRequest(HttpConnection *conn);
|
||||
double tick_duration_s;
|
||||
double dynamic_conn_limit;
|
||||
};
|
||||
93
src/measurement/wsuploadtask.cpp
Normal file
93
src/measurement/wsuploadtask.cpp
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "wsuploadtask.h"
|
||||
#include <vector>
|
||||
|
||||
WsUploadTask::WsUploadTask(const std::string &ticket, const HttpHost &server,
|
||||
unsigned int no_conn, unsigned int max_conn,
|
||||
double duration, double max_time, double tick_s) :
|
||||
ProgressTask("upload", ticket, server, no_conn, max_conn, duration, max_time),
|
||||
tick_duration_s(tick_s>0 ? tick_s : 0.1) {
|
||||
}
|
||||
|
||||
double WsUploadTask::start() {
|
||||
log() << "WsUploadTask starting";
|
||||
checkConnectionCount();
|
||||
return tick_duration_s;
|
||||
}
|
||||
|
||||
void WsUploadTask::newRequest(HttpClientConnection *conn) {
|
||||
// conn->dbgOn();
|
||||
conn->ws_get("/ws?t=" + t());
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::vector<char> getUploadBuffer(size_t len) {
|
||||
std::vector<char> buf;
|
||||
// Fill the buffer with dummy data that cannot cause data compression
|
||||
while (len) {
|
||||
buf.push_back(static_cast<char>(rand()));
|
||||
--len;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
bool WsUploadTask::nextRequest(HttpConnection *conn) {
|
||||
size_t len = loadSize();
|
||||
if (!len)
|
||||
return false;
|
||||
conn->startWsBinStream(len);
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t WsUploadTask::sendWsData(HttpConnection *conn) {
|
||||
static std::vector<char> buffer = getUploadBuffer(post_buffer_len);
|
||||
|
||||
size_t to_write = std::min(conn->wsOutgoingBytesLeft(), post_buffer_len);
|
||||
//log() << "sendWsData " << to_write;
|
||||
return conn->sendData(buffer.data(), to_write);
|
||||
}
|
||||
|
||||
double WsUploadTask::timerEvent() {
|
||||
double time = elapsed();
|
||||
|
||||
if (time >= timeout_s()) {
|
||||
log() << "UploadTask timeout after " << time << " seconds";
|
||||
setResult("-1");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (time > 0.05) {
|
||||
double mbps = addOverheadMbps(byteCount(), time);
|
||||
doTestProgress(mbps, time, currentNoConnections());
|
||||
}
|
||||
|
||||
return tick_duration_s;
|
||||
}
|
||||
|
||||
bool WsUploadTask::websocketUpgrade(HttpClientConnection *conn) {
|
||||
return nextRequest(conn);
|
||||
}
|
||||
|
||||
bool WsUploadTask::wsBinMessage(HttpConnection *,
|
||||
const std::string &msg) {
|
||||
log() << "Unexpected ws bin message, size=" << msg.size();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WsUploadTask::wsTextMessage(HttpConnection *conn,
|
||||
const std::string &msg) {
|
||||
//log() << "WsUploadTask got ws text message: " << msg;
|
||||
std::istringstream ss(msg);
|
||||
std::string op;
|
||||
size_t count;
|
||||
if (ss >> op >> count && op == "upload") {
|
||||
notifyBytesLoaded(count);
|
||||
return nextRequest(conn);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t WsUploadTask::post_buffer_len;
|
||||
28
src/measurement/wsuploadtask.h
Normal file
28
src/measurement/wsuploadtask.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "progresstask.h"
|
||||
|
||||
class WsUploadTask : public ProgressTask {
|
||||
public:
|
||||
WsUploadTask(const std::string &ticket, const HttpHost &server,
|
||||
unsigned int no_conn = 4, unsigned int max_conn = 20,
|
||||
double duration = 10.0, double max_time = 25.0,
|
||||
double tick_s = 0.1);
|
||||
double start() override;
|
||||
double timerEvent() override;
|
||||
void newRequest(HttpClientConnection *conn) override;
|
||||
bool websocketUpgrade(HttpClientConnection *conn) override;
|
||||
bool wsBinMessage(HttpConnection *conn,
|
||||
const std::string &msg) override;
|
||||
bool wsTextMessage(HttpConnection *conn,
|
||||
const std::string &msg) override;
|
||||
size_t sendWsData(HttpConnection *conn) override;
|
||||
private:
|
||||
bool nextRequest(HttpConnection *conn);
|
||||
double tick_duration_s;
|
||||
static const size_t post_buffer_len = 131072;
|
||||
std::map<HttpClientConnection *, size_t> bytes_left_to_post, post_size;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue