// Copyright (C) 2014-2024 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#ifndef WATCH_SOCKET_H
#define WATCH_SOCKET_H

/// @file watch_socket.h Defines the class, WatchSocket.

#include <exceptions/exceptions.h>
#include <boost/noncopyable.hpp>
#include <boost/shared_ptr.hpp>

#include <stdint.h>
#include <string>

namespace isc {
namespace util {

/// @brief Exception thrown if an error occurs during IO source open.
class WatchSocketError : public isc::Exception {
public:
    WatchSocketError(const char* file, size_t line, const char* what) :
        isc::Exception(file, line, what) { }
};

/// @brief Provides an IO "ready" semaphore for use with select() or poll()
/// WatchSocket exposes a single open file descriptor, the "select-fd" which
/// can be marked as being ready to read (i.e. !EWOULDBLOCK) and cleared
/// (i.e. EWOULDBLOCK).  The select-fd can be used with select(), poll(), or
/// their variants alongside other file descriptors.
///
/// Internally, WatchSocket uses a pipe.  The select-fd is the "read" end of
/// pipe.  To mark the socket as ready to read, an integer marker is written
/// to the pipe.  To clear the socket, the marker is read from the pipe.  Note
/// that WatchSocket will only write the marker if it is not already marked.
/// This prevents the socket's pipe from filling endlessly.
///
/// @warning Because the read or "sink" side of the pipe is used as the select_fd,
/// it is possible for that fd to be interfered with, albeit only from within the
/// process space which owns it.  Performing operations that may alter the fd's state
/// such as close, read, or altering behavior flags with fcntl or ioctl can have
/// unpredictable results.  It is intended strictly use with functions such as select()
/// poll() or their variants.
class WatchSocket : public boost::noncopyable {
public:
    /// @brief Value used to signify an invalid descriptor.
    static const int SOCKET_NOT_VALID = -1;
    /// @brief Value written to the source when marking the socket as ready.
    /// The value itself is arbitrarily chosen as one that is unlikely to occur
    /// otherwise and easy to debug.
    static const uint32_t MARKER = 0xDEADBEEF;

    /// @brief Constructor
    ///
    /// Constructs an instance of the WatchSocket in the cleared (EWOULDBLOCK)
    /// state.
    WatchSocket();

    /// @brief Destructor
    ///
    /// Closes all internal resources, including the select-fd.
    virtual ~WatchSocket();

    /// @brief Marks the select-fd as ready to read.
    ///
    /// Marks the socket as ready to read, if is not already so marked.
    /// If an error occurs, closeSocket is called. This will force any further
    /// use of the select_fd to fail rather than show the fd as READY.  Such
    /// an error is almost surely a programmatic error which has corrupted the
    /// select_fd.
    ///
    /// @throw WatchSocketError if an error occurs marking the socket.
    void markReady();

    /// @brief Returns true the if socket is marked as ready.
    ///
    /// This method uses a non-blocking call to  select() to test read state of the
    /// select_fd.  Rather than track what the status "should be" it tests the status.
    /// This should eliminate conditions where the select-fd appear to be perpetually
    /// ready.
    /// @return  Returns true if select_fd is not SOCKET_NOT_VALID and select() reports it
    /// as !EWOULDBLOCK, otherwise it returns false.
    /// This method is guaranteed NOT to throw.
    bool isReady();

    /// @brief Clears the socket's ready to read marker.
    ///
    /// Clears the socket if it is currently marked as ready to read.
    /// If an error occurs, closeSocket is called. This will force any further
    /// use of the select_fd to fail rather than show the fd as READY.  Such
    /// an error is almost surely a programmatic error which has corrupted the
    /// select_fd.
    ///
    /// @throw WatchSocketError if an error occurs clearing the socket
    /// marker.
    void clearReady();

    /// @brief Returns the file descriptor to use to monitor the socket.
    ///
    /// @note Using this file descriptor as anything other than an argument
    /// to select() or similar methods can have unpredictable results.
    ///
    /// @return The file descriptor associated with read end of the socket's
    /// pipe.
    int getSelectFd();

    /// @brief Closes the descriptors associated with the socket.
    ///
    /// This method is used to close the socket and capture errors that
    /// may occur during this operation.
    ///
    /// @param [out] error_string Holds the error string if closing
    /// the socket failed. It will hold empty string otherwise.
    ///
    /// @return true if the operation was successful, false otherwise.
    bool closeSocket(std::string& error_string);

private:

    /// @brief Closes the descriptors associated with the socket.
    ///
    /// This method is called by the class destructor and it ignores
    /// any errors that may occur while closing the sockets.
    void closeSocket();

    /// @brief The end of the pipe to which the marker is written
    int source_;

    /// @brief The end of the pipe from which the marker is read.
    /// This is the value returned as the select-fd.
    int sink_;
};

/// @brief Defines a smart pointer to an instance of a WatchSocket.
typedef boost::shared_ptr<WatchSocket> WatchSocketPtr;

} // namespace isc::util
} // namespace isc

#endif
