diff --git a/.gitignore b/.gitignore index 8eae4dd..3f988dd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,7 @@ .DS_Store .vscode + +a.out # Personal concept notes (learn-concept skill) concepts/ diff --git a/include/server/Client.hpp b/include/server/Client.hpp new file mode 100644 index 0000000..a855c7f --- /dev/null +++ b/include/server/Client.hpp @@ -0,0 +1,166 @@ +#ifndef CLIENT_HPP +#define CLIENT_HPP + +#include + +/* +** Client +** ------ +** Goal: Représenter l'état d'UNE connexion TCP active. C'est l'objet +** central qui fait le pont entre Core Server et HTTP Layer. +** +** Cette classe ne fait AUCUN appel système réseau elle-même (pas de +** read/write directement ici idéalement — voir note ci-dessous). +** Elle stocke juste l'état : ce qu'on a reçu, ce qu'on doit envoyer, +** et où on en est dans le cycle de vie de la requête. +** +** C'est l'objet que Server manipule à chaque tour de boucle : +** - Server fait read() → range les octets dans client.read_buffer +** - HTTP Layer lit client.read_buffer → remplit client.write_buffer +** - Server fait write() → vide client.write_buffer +** +** Note: selon ton design, tu peux mettre les appels read()/write() +** directement dans des méthodes de Client (receiveData/sendData) — +** c'est acceptable et même conseillé pour garder Server léger. +** Le prototype ci-dessous part sur cette option. +*/ + +// État du cycle de vie d'une requête — utile pour savoir +// "où on en est" sans tout recalculer à chaque tour de poll(). +enum ClientState +{ + READING_REQUEST, // on accumule read_buffer, requête pas encore complète + PROCESSING, // requête complète, HTTP Layer doit la traiter + SENDING_RESPONSE, // write_buffer prêt, on envoie au client + CGI_RUNNING, // en attente de la sortie d'un processus CGI + DONE, // réponse envoyée — fermer ou attendre keep-alive + CLOSING // erreur ou déconnexion — à nettoyer +}; + +class Client +{ + private: + int _fd; + int _server_port; + ClientState _state; + + std::string _read_buffer; + std::string _write_buffer; + size_t _write_offset; + + long _last_activity; + + // Non copyable : un Client possède un fd unique + Client(const Client& other); + Client& operator=(const Client& other); + + public: + /* + ** Goal: Initialiser un client à partir du fd retourné par accept(). + ** server_port permet de retrouver le bon ServerConfig (multi-port) + ** sans avoir à le chercher à chaque requête. + */ + Client(int fd, int server_port); + ~Client(); + + // --- Accès au fd --- + + /* + ** Goal: Retourne le fd de ce client. + ** Utilisé par Server pour PollManager::isReadable(getFd()) etc. + */ + int getFd() const; + + // --- Lecture (déclenché par PollManager::isReadable) --- + + /* + ** Goal: Appelle read() sur le fd et accumule dans read_buffer. + ** Retourne : + ** > 0 : nombre d'octets lus (normal, continuer) + ** == 0 : le client a fermé la connexion (EOF) → state = CLOSING + ** < 0 : erreur read() → state = CLOSING (ne PAS check errno + ** pour décider du comportement HTTP, juste fermer proprement) + ** + ** Après chaque appel réussi, vérifie isRequestComplete() et passe + ** state à PROCESSING si c'est le cas. + */ + int receiveData(); + + /* + ** Goal: Vérifie si read_buffer contient une requête HTTP complète. + ** - Sans body : présence de "\r\n\r\n" + ** - Avec body : présence de "\r\n\r\n" ET + ** read_buffer.size() - header_end >= Content-Length + ** + ** Cette logique est dupliquée/déléguée avec RequestParser — + ** à toi de choisir : soit Client appelle un helper de + ** RequestParser::isComplete(), soit Client a sa propre version + ** légère. Recommandation : déléguer à RequestParser pour éviter + ** la duplication de logique de parsing. + */ + bool isRequestComplete() const; + + // --- Écriture (déclenché par PollManager::isWritable) --- + + /* + ** Goal: Appelle write() avec write_buffer et avance _write_offset. + ** Gère le "partial write" : write() peut envoyer moins d'octets + ** que demandé, il NE FAUT PAS considérer ça comme une erreur. + ** + ** Retourne : + ** > 0 : octets envoyés (continuer au prochain poll si incomplet) + ** == 0 : rien envoyé (peu probable si POLLOUT était set, mais + ** ne pas crasher) + ** < 0 : erreur → state = CLOSING + ** + ** Quand _write_offset == write_buffer.size() → state = DONE, + ** et PollManager doit repasser ce fd en POLLIN seulement + ** (ou removeFd si pas de keep-alive). + */ + int sendData(); + + /* + ** Goal: Indique si write_buffer a été entièrement envoyé. + ** Utilisé par Server pour savoir quand basculer + ** updateEvents(fd, POLLIN) (retirer POLLOUT). + */ + bool isResponseFullySent() const; + + // --- État et accesseurs --- + + ClientState getState() const; + void setState(ClientState state); + + /* + ** Goal: Accès en lecture/écriture au buffer de requête brut. + ** L'HTTP Layer (RequestParser) lit ce buffer pour produire + ** un HttpRequest. Après parsing réussi, le buffer est vidé + ** (clear) ou avancé (pour gérer le pipelining keep-alive — + ** optionnel selon votre ambition). + */ + std::string& getReadBuffer(); + + /* + ** Goal: Définit la réponse complète à envoyer. + ** Appelé par HTTP Layer (ResponseBuilder) une fois la réponse + ** assemblée. Remet _write_offset à 0 et state = SENDING_RESPONSE. + */ + void setWriteBuffer(const std::string& response); + + /* + ** Goal: Port du serveur sur lequel ce client s'est connecté. + ** Permet au Router de retrouver le bon ServerConfig + ** (cas multi-port avec configs différentes). + */ + int getServerPort() const; + + /* + ** Goal: Timestamp de la dernière activité (read ou write réussi). + ** Utilisé par Server pour détecter et fermer les connexions + ** inactives (anti "hang indéfiniment", règle du sujet). + */ + long getLastActivity() const; + void updateLastActivity(); +}; + +#endif \ No newline at end of file diff --git a/include/server/PollManager.hpp b/include/server/PollManager.hpp new file mode 100644 index 0000000..d457437 --- /dev/null +++ b/include/server/PollManager.hpp @@ -0,0 +1,100 @@ +#ifndef POLLMANAGER_HPP +#define POLLMANAGER_HPP + +#include +#include +#include + +/* +** PollManager +** ----------- +** Goal: Centraliser la liste des file descriptors surveillés par poll() +** et exposer une interface simple pour les ajouter/retirer/interroger. +** +** Cette classe NE SAIT PAS ce qu'est un client HTTP, un CGI ou un socket +** d'écoute. Elle manipule uniquement des entiers (fd) et des flags +** (POLLIN, POLLOUT). C'est la frontière la plus basse du serveur. +** +** Règle absolue : un seul poll() pour tout le programme. Cette classe +** EST ce poll() unique. +*/ +class PollManager +{ + private: + std::vector _fds; + + /* + ** Goal: Trouver l'index dans _fds correspondant à un fd donné. + ** Retourne -1 si non trouvé. + ** Interne uniquement — utilisé par updateEvents/removeFd/isReadable... + */ + int findIndex(int fd) const; + + public: + PollManager(); + ~PollManager(); + + int getSize() const; + + /* + ** Goal: Ajouter un fd à la liste surveillée par poll(). + ** events = POLLIN, POLLOUT, ou POLLIN | POLLOUT + ** Appelé quand : un nouveau client se connecte (accept), + ** un CGI démarre (pipe), etc. + */ + void addFd(int fd, short events); + + /* + ** Goal: Retirer un fd de la liste. + ** Appelé quand : un client se déconnecte, un CGI a fini, + ** une erreur fatale sur ce fd. + ** Important : NE FAIT PAS le close() du fd. C'est la responsabilité + ** de l'appelant (Server/Client) de fermer le fd lui-même. + */ + void removeFd(int fd); + + /* + ** Goal: Modifier les events surveillés pour un fd existant. + ** Cas d'usage typique : un Client a fini de lire (POLLIN suffit) + ** puis a une réponse à envoyer (passer à POLLIN | POLLOUT). + ** Si fd non trouvé : ne fait rien (no-op silencieux). + */ + void updateEvents(int fd, short events); + + /* + ** Goal: Appeler poll() sur la liste actuelle. + ** timeout en millisecondes (ex: 1000 = 1s). Utiliser un timeout + ** fini (pas -1) pour pouvoir détecter les clients inactifs + ** (cf. gestion des timeouts côté Server). + ** Retourne : nombre de fds prêts (>= 0), ou -1 en cas d'erreur + ** (errno EINTR à gérer : ne pas crasher, juste re-looper). + */ + int pollEngine(int timeout_ms); + + /* + ** Goal: Après un poll() réussi, indique si ce fd est prêt en lecture. + ** Utilisé par Server pour décider d'appeler read() sur ce fd. + */ + bool isReadable(int fd) const; + + /* + ** Goal: Idem mais pour l'écriture (POLLOUT déclenché). + ** Utilisé par Server pour décider d'appeler write() sur ce fd. + */ + bool isWritable(int fd) const; + + /* + ** Goal: Détecte une erreur/hangup sur ce fd (POLLHUP, POLLERR, POLLNVAL). + ** Si true, le fd doit être nettoyé (close + removeFd) sans tenter + ** read/write supplémentaire. + */ + bool hasError(int fd) const; + + /* + ** Goal: Exposer la liste complète des fds actuellement surveillés. + ** Utile pour itérer dans Server::run() après chaque poll(). + */ + const std::vector& getFds() const; +}; + +#endif \ No newline at end of file diff --git a/include/server/README.md b/include/server/README.md new file mode 100644 index 0000000..1d27fe6 --- /dev/null +++ b/include/server/README.md @@ -0,0 +1,44 @@ +## Comment lire ces fichiers + +**PollManager** — la brique la plus basse. Encapsule `poll()` et le +tableau de `pollfd`. Tu codes ça en premier, et tu peux le tester tout +seul avec un petit `main()` qui ajoute `stdin` (fd 0) et vérifie que +`isReadable(0)` devient vrai quand tu tapes au clavier. + +**Socket** — un wrapper sur `socket`/`bind`/`listen`/`accept`. Une +instance par port à écouter. Le commentaire détaille l'ordre exact des +syscalls et pourquoi (notamment `SO_REUSEADDR` et `O_NONBLOCK`, qui sont +des pièges classiques si oubliés). + +**Client** — c'est le cœur de ton travail. C'est l'objet qui porte +l'état d'une connexion. J'ai volontairement mis `receiveData()` et +`sendData()` dans cette classe (et pas dans `Server`) pour que la +logique de "partial read/write" et de buffers soit localisée à un seul +endroit — c'est là que tu vas passer le plus de temps et où les edge +cases (EOF, partial write, requête incomplète) se concentrent. + +**Server** — orchestrateur. La boucle `run()` est décrite en +pseudo-code dans le commentaire — c'est le squelette que tu dois +remplir. Les 4 handlers privés (`handleNewConnection`, +`handleClientRead`, `handleClientWrite`, `closeClient`) sont les seuls +endroits où tu touches aux fds. + +## Core server order + +1. **`src/server/PollManager.cpp`** + Implémente les 8 méthodes. + Teste avec `tests/test_poll_manager.cpp` + (ajoute `stdin`, vérifie `isReadable(0)`). + +2. **`src/server/Socket.cpp`** + Implémente le constructeur + (les 5 étapes du commentaire) + et `acceptConnection()`. + +3. **`src/server/Client.cpp`** + `receiveData()` et `sendData()` sont les plus importants. + Commence par une version simple, + ajoute le _partial write_ ensuite. + +4. **`src/server/Server.cpp`** + Remplis `run()` en suivant le pseudo‑code du header. diff --git a/include/server/Server.hpp b/include/server/Server.hpp new file mode 100644 index 0000000..eb6d997 --- /dev/null +++ b/include/server/Server.hpp @@ -0,0 +1,140 @@ +#ifndef SERVER_HPP +#define SERVER_HPP + +#include +#include +#include "Socket.hpp" +#include "Client.hpp" +#include "PollManger.hpp" +#include "../../config/default.conf" + +/* +** Server +** ------ +** Goal: Chef d'orchestre. Possède tous les Socket d'écoute, tous les +** Client connectés, et LE PollManager unique. Contient la boucle +** principale run(). +** +** C'est la SEULE classe qui doit connaître à la fois : +** - les sockets d'écoute (Socket) +** - les clients connectés (Client) +** - le poll() (PollManager) +** - et qui DOIT appeler l'HTTP Layer (via un point d'entrée unique, +** ex: HttpDispatcher::handle(client, configs) — à définir avec +** l'équipe HTTP Layer comme LE point de contact entre les 2 modules) +** +** Tout le reste (Socket, Client, PollManager) ne sait rien des autres +** classes du Core Server — c'est Server qui les fait collaborer. +*/ +class Server +{ + public: + Server(); + ~Server(); + + /* + ** Goal: Enregistrer une config serveur (= un ou plusieurs + ** host:port à écouter). Appelé depuis main() après le parsing + ** de la config, AVANT run(). + ** Crée en interne le(s) Socket correspondant(s) et les ajoute + ** au PollManager (POLLIN, car on attend des connexions). + */ + void addServerConfig(const ServerConfig& config); + + /* + ** Goal: LA boucle principale. Ne retourne jamais sauf signal + ** d'arrêt (ex: SIGINT proprement géré) ou erreur fatale. + ** + ** Pseudo-code de la boucle (1 tour = 1 itération) : + ** + ** while (running) { + ** poll_manager.poll(TIMEOUT_MS); + ** + ** for each fd in poll_manager.getFds(): + ** if fd == un des listening sockets: + ** if isReadable(fd): handleNewConnection(fd); + ** else: + ** if hasError(fd): closeClient(fd); + ** else if isReadable(fd): handleClientRead(fd); + ** else if isWritable(fd): handleClientWrite(fd); + ** + ** checkTimeouts(); // déconnecte les clients inactifs + ** } + ** + ** Règle absolue : TOUT read/write passe par les handlers privés + ** ci-dessous, jamais d'appel direct dans run(). + */ + void run(); + + /* + ** Goal: Demande l'arrêt propre de la boucle (ex: appelé depuis + ** un signal handler SIGINT). Met _running = false. + ** La boucle actuelle finit son tour puis sort de run(). + */ + void stop(); + + private: + std::vector _listening_sockets; + std::map _clients; // fd -> Client* + std::map _configs_by_port; // port -> config (multi-port) + PollManager _poll_manager; + bool _running; + + /* + ** Goal: Un socket d'écoute a un événement POLLIN → accept(). + ** Crée un nouveau Client, l'ajoute à _clients et au PollManager + ** (POLLIN seulement au début — on n'a rien à écrire encore). + */ + void handleNewConnection(int listening_fd); + + /* + ** Goal: Un client a des données à lire. + ** Appelle client->receiveData(). + ** Si state == CLOSING (déconnexion/erreur) → closeClient(fd). + ** Si state == PROCESSING (requête complète) → appelle le point + ** d'entrée HTTP Layer pour obtenir la réponse, puis + ** client->setWriteBuffer(response) et + ** poll_manager.updateEvents(fd, POLLIN | POLLOUT). + */ + void handleClientRead(int fd); + + /* + ** Goal: Un client est prêt à recevoir des données. + ** Appelle client->sendData(). + ** Si isResponseFullySent() : + ** - si keep-alive : updateEvents(fd, POLLIN), state = READING_REQUEST, + ** vider read_buffer pour la prochaine requête sur ce fd + ** - sinon : closeClient(fd) + ** Si state == CLOSING → closeClient(fd). + */ + void handleClientWrite(int fd); + + /* + ** Goal: Fermeture propre d'un client. + ** Fait : close(fd), poll_manager.removeFd(fd), + ** delete _clients[fd], _clients.erase(fd). + ** Centraliser ici évite les fuites de fd dispersées dans le code. + */ + void closeClient(int fd); + + /* + ** Goal: Parcourt _clients et ferme ceux dont + ** getLastActivity() dépasse un seuil (ex: 60s sans activité). + ** Implémente la règle "une requête ne doit jamais hang indéfiniment". + ** Appelé une fois par tour de boucle, après le traitement des fds. + */ + void checkTimeouts(); + + /* + ** Goal: Retrouve le ServerConfig correspondant au port sur lequel + ** le client est connecté. Utilisé pour passer la bonne config + ** (routes, error pages, max body size) au Router de l'HTTP Layer. + */ + const ServerConfig& getConfigForClient(const Client* client) const; + + // Non copyable + Server(const Server& other); + Server& operator=(const Server& other); +}; + +#endif \ No newline at end of file diff --git a/include/server/Socket.hpp b/include/server/Socket.hpp new file mode 100644 index 0000000..0c28458 --- /dev/null +++ b/include/server/Socket.hpp @@ -0,0 +1,82 @@ +#ifndef SOCKET_HPP +#define SOCKET_HPP + +#include + +/* +** Socket +** ------ +** Goal: Encapsuler la création d'un socket d'écoute serveur : +** socket() + setsockopt() + bind() + listen(). +** +** Une instance = un socket d'écoute pour une combinaison host:port +** définie dans la config. Si la config définit 3 ports, tu crées +** 3 instances de Socket. +** +** Cette classe NE GÈRE PAS les clients connectés (c'est Client) et +** NE FAIT PAS de poll() elle-même (c'est PollManager). Elle fournit +** juste un fd prêt à être surveillé. +*/ +class Socket +{ + public: + /* + ** Goal: Construire et configurer le socket d'écoute. + ** host = "0.0.0.0" ou une IP spécifique (ex: "127.0.0.1") + ** port = ex 8080 + ** + ** Fait, dans l'ordre : + ** 1. socket(AF_INET, SOCK_STREAM, 0) + ** 2. setsockopt(SO_REUSEADDR) — évite "Address already in use" + ** au redémarrage rapide du serveur + ** 3. fcntl(fd, F_SETFL, O_NONBLOCK) — OBLIGATOIRE, le sujet + ** interdit tout fd bloquant + ** 4. bind() sur host:port + ** 5. listen() avec un backlog raisonnable (ex: 128) + ** + ** En cas d'échec à une étape : throw une exception ou retourne + ** un état d'erreur — à toi de choisir la stratégie d'erreur, + ** mais le programme ne doit JAMAIS crash (cf. règles générales). + */ + Socket(const std::string& host, int port); + ~Socket(); + + /* + ** Goal: Retourner le fd du socket d'écoute. + ** Utilisé pour : PollManager::addFd(getFd(), POLLIN) + ** et pour comparer fds[i].fd == getFd() dans Server::run() + ** afin de savoir "est-ce un nouveau client ou un client existant ?" + */ + int getFd() const; + + /* + ** Goal: Accepter une nouvelle connexion entrante. + ** Doit être appelé UNIQUEMENT quand PollManager::isReadable(getFd()) + ** est true (sinon violation de la règle "jamais sans poll()"). + ** + ** Retourne : le fd du nouveau client (>= 0), ou -1 si accept() + ** échoue (ex: EAGAIN car un autre thread/process l'a pris avant — + ** improbable ici mais à gérer sans crash). + ** + ** Le fd retourné doit être mis en O_NONBLOCK avant d'être utilisé. + */ + int acceptConnection() const; + + /* + ** Goal: Retourne le port sur lequel ce socket écoute. + ** Utile pour le logging / debug et pour faire correspondre + ** une connexion entrante à son ServerConfig (multi-port). + */ + int getPort() const; + + private: + int _fd; + std::string _host; + int _port; + + // Non copyable : un Socket possède un fd unique (ressource OS) + Socket(const Socket& other); + Socket& operator=(const Socket& other); +}; + +#endif \ No newline at end of file diff --git a/include/server/server-checklist.md b/include/server/server-checklist.md new file mode 100644 index 0000000..05a94ff --- /dev/null +++ b/include/server/server-checklist.md @@ -0,0 +1,70 @@ +# Webserv — Checklist complète du projet + +--- + +## 🟪 Module Core Server (event loop) + +### 1 — PollManager + +- [ ] `addFd(fd, events)` +- [ ] `removeFd(fd)` +- [ ] `updateEvents(fd, events)` +- [ ] `poll(timeout_ms)` — gérer le retour -1 avec EINTR sans crash +- [ ] `isReadable(fd)` +- [ ] `isWritable(fd)` +- [ ] `hasError(fd)` — détecter POLLHUP / POLLERR / POLLNVAL +- [ ] `getFds()` +- [ ] Test isolé : ajouter `stdin` (fd 0), taper au clavier, vérifier `isReadable(0)` + +### 2 — Socket + +- [ ] Constructeur : `socket()` +- [ ] `setsockopt(SO_REUSEADDR)` +- [ ] Passage en non-bloquant (`fcntl` + `O_NONBLOCK`) +- [ ] `bind()` sur host:port +- [ ] `listen()` avec backlog +- [ ] Gestion d'erreur sur chaque étape (pas de crash, message clair) +- [ ] `getFd()` +- [ ] `getPort()` +- [ ] `acceptConnection()` — retourne le fd du nouveau client, le passe en non-bloquant +- [ ] Test : `./webserv` démarre et écoute (vérifiable avec `lsof -i :8080` ou `netstat`) + +### 3 — Client + +- [ ] Constructeur (fd, server_port) +- [ ] `getFd()` +- [ ] `receiveData()` — read() + accumulation dans read_buffer +- [ ] Gérer read() qui retourne 0 → EOF → state = CLOSING +- [ ] Gérer read() qui retourne -1 → erreur → state = CLOSING (sans check errno pour la logique) +- [ ] `isRequestComplete()` — détecte `\r\n\r\n` + vérifie Content-Length si présent +- [ ] `sendData()` — write() avec gestion du **partial write** (offset) +- [ ] `isResponseFullySent()` +- [ ] `getState()` / `setState()` +- [ ] `getReadBuffer()` / `setWriteBuffer()` +- [ ] `getServerPort()` +- [ ] `getLastActivity()` / `updateLastActivity()` +- [ ] Destructeur (pas de close() ici si géré par Server::closeClient) + +### 4 — Server (boucle principale) + +- [ ] `addServerConfig()` — crée les `Socket` correspondants, les ajoute au PollManager (POLLIN) +- [ ] `run()` : boucle infinie avec `poll_manager.poll(timeout)` +- [ ] Distinguer "fd = socket d'écoute" vs "fd = client existant" +- [ ] `handleNewConnection()` — accept() + création Client + ajout PollManager +- [ ] `handleClientRead()` — receiveData() + appel HTTP Layer si requête complète +- [ ] `handleClientWrite()` — sendData() + gestion fin d'envoi (keep-alive ou close) +- [ ] `closeClient()` — close(fd) + removeFd + delete + erase (centralisé, aucune fuite de fd) +- [ ] `checkTimeouts()` — fermer les clients inactifs depuis > X secondes +- [ ] `getConfigForClient()` — retrouver le bon ServerConfig selon le port +- [ ] `stop()` — arrêt propre (SIGINT géré sans crash) +- [ ] **Vérifier : aucun read/write n'est appelé sans passage par poll() avant** +- [ ] **Vérifier : un seul poll() pour tout le programme (listen + clients + CGI pipes)** + +### 5 — Tests Core Server + +- [ ] Test manuel telnet : connexion acceptée, log affiché +- [ ] Test manuel telnet : déconnexion détectée proprement (pas de crash) +- [ ] Test manuel telnet : requête envoyée en plusieurs morceaux → bien accumulée +- [ ] Test : 2 clients connectés simultanément, chacun reçoit sa propre réponse +- [ ] Test : client qui n'envoie rien pendant longtemps → timeout déclenché et connexion fermée +- [ ] Test : grosse requête (plusieurs Mo) → pas de crash, lue en plusieurs read() diff --git a/src/server/PollManager.cpp b/src/server/PollManager.cpp new file mode 100644 index 0000000..933c3a7 --- /dev/null +++ b/src/server/PollManager.cpp @@ -0,0 +1,83 @@ +#include "../../include/server/PollManager.hpp" + +PollManager::PollManager() { + _fds.reserve(5); + std::cout << "[PollManager] ctor called\n"; +}; + +PollManager::~PollManager() { + std::cout << "[PollManager] dtor called\n"; +}; + +// ---------------------- PRIVATE ---------------------- + +int PollManager::findIndex(int fd) const { + std::size_t i = 0; + while (_fds[i].fd != fd) { + i++; + }; + return i; +}; + +// ---------------------- PUBLIC ---------------------- + +int PollManager::getSize() const { + return _fds.size(); +}; + +void PollManager::addFd(int fd, short events) { + struct pollfd newFd; + + newFd.fd = fd; + newFd.events = events; + _fds.push_back(newFd); +}; + +void PollManager::removeFd(int fd) { + size_t idx = findIndex(fd); + if (idx < _fds.size()) { + _fds.erase(_fds.begin() + idx); + } +}; + +void PollManager::updateEvents(int fd, short events) { + int idx = findIndex(fd); + _fds[idx].events = events; +}; + +int PollManager::pollEngine(int timeout_ms) { + int poll_count = poll(&_fds[0], _fds.size(), timeout_ms); + + return poll_count; +}; + +bool PollManager::isReadable(int fd) const { + int idx = findIndex(fd); + + if (_fds[idx].revents & POLLIN) { + return true; + } + return false; +}; + +bool PollManager::isWritable(int fd) const { + int idx = findIndex(fd); + + if (_fds[idx].revents & POLLOUT) { + return true; + } + return false; +}; + +bool PollManager::hasError(int fd) const { + int idx = findIndex(fd); + + if (_fds[idx].revents & (POLLHUP | POLLNVAL | POLLERR)) { + return true; + } + return false; +}; + +const std::vector& PollManager::getFds() const { + return _fds; +}; \ No newline at end of file diff --git a/tests/server/test_poll_manager.cpp b/tests/server/test_poll_manager.cpp new file mode 100644 index 0000000..eed5d83 --- /dev/null +++ b/tests/server/test_poll_manager.cpp @@ -0,0 +1,187 @@ +/* +** test_poll_manager.cpp +** ---------------------- +** Goal: Valider chaque méthode de PollManager AVANT de coder Socket +** et Server. On utilise stdin (fd 0) comme "fausse source +** d'événements" — pas besoin de sockets pour ce test. +** +** Compilation : +** c++ -Wall -Wextra -Werror -std=c++98 -I ../include \ +** test_poll_manager.cpp ../src/server/PollManager.cpp \ +** -o test_poll_manager +** +** Exécution : +** ./test_poll_manager +** -> puis tape n'importe quoi au clavier + Entrée +** -> le programme doit afficher "stdin is readable!" et quitter +** proprement si tu tapes "exit" +** +** Ce que ce test prouve, étape par étape : +** 1. addFd() ajoute bien fd 0 à la liste +** 2. poll() ne bloque pas indéfiniment (timeout visible toutes les 2s) +** 3. isReadable() détecte correctement quand stdin a des données +** 4. hasError() ne se déclenche jamais sur un fd normal +** 5. updateEvents() ne casse rien quand on l'appelle +** 6. removeFd() retire bien le fd (plus aucun event après) +*/ + +int main() +{ + return 0; +} + +// #include "../../include/server/PollManager.hpp" +// #include +// #include +// #include + +// // Petite aide visuelle pour rendre le test lisible dans le terminal +// static void printStep(const std::string& step) +// { +// std::cout << "\n=== " << step << " ===" << std::endl; +// } + +// int main() +// { +// PollManager poll_manager; + +// // ------------------------------------------------------------ +// // ETAPE 1 : addFd() — on surveille stdin en lecture +// // ------------------------------------------------------------ +// printStep("Etape 1 : addFd(STDIN_FILENO, POLLIN)"); +// poll_manager.addFd(STDIN_FILENO, POLLIN); + +// assert(poll_manager.getFds().size() == 1); +// assert(poll_manager.getFds()[0].fd == STDIN_FILENO); +// std::cout << "OK : stdin (fd " << STDIN_FILENO +// << ") ajoute a la liste. Taille liste = " +// << poll_manager.getFds().size() << std::endl; + +// // ------------------------------------------------------------ +// // ETAPE 2 : poll() avec timeout — ne doit jamais bloquer pour rien +// // ------------------------------------------------------------ +// printStep("Etape 2 : poll() avec timeout de 2000ms"); +// std::cout << "Tape quelque chose au clavier puis Entree." << std::endl; +// std::cout << "(si tu ne tapes rien, le programme affichera" +// " 'timeout, rien de pret' toutes les 2s)" << std::endl; +// std::cout << "Tape 'exit' + Entree pour quitter proprement.\n" << std::endl; + +// bool running = true; +// int loop_count = 0; + +// while (running) +// { +// int ready = poll_manager.pollEngine(2000); // 2000ms = 2s de timeout + +// loop_count++; + +// // -------------------------------------------------------- +// // Cas A : poll() retourne -1 -> erreur +// // -------------------------------------------------------- +// if (ready < 0) +// { +// std::cout << "poll() a retourne une erreur (ready=-1)." +// << " Si c'est EINTR, c'est normal (signal recu)." +// << " On continue sans crasher." << std::endl; +// continue; +// } + +// // -------------------------------------------------------- +// // Cas B : poll() retourne 0 -> timeout, personne n'est pret +// // -------------------------------------------------------- +// if (ready == 0) +// { +// std::cout << "[tour " << loop_count << "] timeout, rien de pret" +// << std::endl; +// continue; +// } + +// // -------------------------------------------------------- +// // Cas C : ready > 0 -> au moins un fd est pret, on inspecte +// // -------------------------------------------------------- +// std::cout << "[tour " << loop_count << "] poll() dit : " +// << ready << " fd(s) pret(s)" << std::endl; + +// // ETAPE 3 : isReadable() +// if (poll_manager.isReadable(STDIN_FILENO)) +// { +// char buffer[256]; +// ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer) - 1); + +// if (n > 0) +// { +// buffer[n] = '\0'; +// std::string input(buffer); + +// // Retirer le \n final pour la comparaison +// if (!input.empty() && input[input.size() - 1] == '\n') +// input.erase(input.size() - 1); + +// std::cout << "OK : stdin is readable! Tu as tape : \"" +// << input << "\"" << std::endl; + +// if (input == "exit") +// { +// std::cout << "Mot 'exit' detecte, on arrete le test." +// << std::endl; +// running = false; +// } +// } +// } + +// // ETAPE 4 : hasError() — ne doit jamais se déclencher ici +// if (poll_manager.hasError(STDIN_FILENO)) +// { +// std::cout << "ATTENTION : hasError() est true sur stdin," +// << " ce qui est inattendu dans ce test simple." +// << std::endl; +// } +// } + +// // ------------------------------------------------------------ +// // ETAPE 5 : updateEvents() — verifie que ca ne crash pas +// // ------------------------------------------------------------ +// printStep("Etape 5 : updateEvents(STDIN_FILENO, POLLIN)"); +// poll_manager.updateEvents(STDIN_FILENO, POLLIN); +// std::cout << "OK : updateEvents() appele sans crash." << std::endl; + +// // Test sur un fd qui n'existe pas dans la liste -> doit etre un no-op +// poll_manager.updateEvents(9999, POLLIN); +// std::cout << "OK : updateEvents() sur un fd inexistant (9999)" +// << " ne crash pas (no-op attendu)." << std::endl; + +// // ------------------------------------------------------------ +// // ETAPE 6 : removeFd() — la liste doit redevenir vide +// // ------------------------------------------------------------ +// printStep("Etape 6 : removeFd(STDIN_FILENO)"); +// poll_manager.removeFd(STDIN_FILENO); + +// assert(poll_manager.getFds().empty()); +// std::cout << "OK : stdin retire. Taille liste = " +// << poll_manager.getFds().size() << std::endl; + +// // removeFd sur un fd deja absent -> ne doit pas crash non plus +// poll_manager.removeFd(STDIN_FILENO); +// std::cout << "OK : removeFd() une seconde fois sur le meme fd" +// << " ne crash pas (no-op attendu)." << std::endl; + +// std::cout << "\n=== TOUS LES TESTS PollManager SONT PASSES ===" +// << std::endl; + +// return 0; +// } + +// Ce test verifie si la detection isReadable fonctionne ainsi que le fait de pouvoir switch pour ecouter le writtable + +// int main(void) +// { +// PollManager pollManager; + +// // std::cout << pollManager.getSize() << std::endl; +// pollManager.addFd(0, POLLIN); +// int pollNum = pollManager.pollEngine(-1); +// std::cout << pollManager.isReadable(0) << std::endl; +// pollManager.updateEvents(0, POLLOUT); +// pollNum = pollManager.pollEngine(-1); +// std::cout << pollManager.isWritable(0) << std::endl; +// } \ No newline at end of file