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