Skip to content

Phase 1.5: load config and initialize Server from ListenSpec list - #14

Merged
MukhammadIbrokhimov merged 13 commits into
mainfrom
feature/phase-1-5-load-config
May 29, 2026
Merged

Phase 1.5: load config and initialize Server from ListenSpec list#14
MukhammadIbrokhimov merged 13 commits into
mainfrom
feature/phase-1-5-load-config

Conversation

@MukhammadIbrokhimov

@MukhammadIbrokhimov MukhammadIbrokhimov commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces the hardcoded single-socket startup in main.cpp with a config-driven flow that opens N listening sockets (one per ListenSpec in cfg.servers()). Server now owns the listeners and maintains an fd → ServerConfig* map that Phase 2.4's router will read.

  • main: argv handling, default-path fallback (config/default.conf), single top-level try/catch printing fatal: <msg> on any std::exception. Existing LEXER_DUMP / CONFIG_DUMP dev hooks preserved per the kanban Phase-4 cleanup schedule.
  • Socket: new ctor Socket(const std::string& host, int port) resolves the host via getaddrinfo(AI_NUMERICHOST | AI_PASSIVE) (no DNS, no blocking), sets SO_REUSEADDR before bind, and follows a strict cleanup contract — every throw path runs freeaddrinfo(res) and ::close(fd) before propagating. Port range guarded (1-65535).
  • Server: new ctor Server(const Config&) walks cfg.servers(), allocates one Socket* per ListenSpec, populates std::map<int, const ServerConfig*> fd_to_server_. Partial-init RAII pattern: the construction loop is wrapped in try { } catch(...) { delete partial state; throw; } to handle the pre-C++11 case where ctor exceptions skip the destructor. Signal handlers install as the last ctor step so the invariant "handlers installed iff Server fully constructed" holds.
  • Legacy paths removed in a dedicated commit (6923124): Socket(int port) and Server(Socket&) no longer exist. Staged refactor kept every interim commit buildable.

Subject-compliance notes:

  • All external functions used (getaddrinfo, freeaddrinfo, setsockopt, fcntl, socket, bind, listen, htons, close) appear in the allowed-functions list on subject p.6.
  • macOS fcntl restriction respected: only F_SETFL + O_NONBLOCK used.
  • Virtual hosting explicitly out of scope per subject p.10; duplicate (host, port) across server blocks is rejected at validate-time (rule already lived in Config::validate from Phase 1.4).
  • Pre-existing risk inherited from earlier phase: sigaction is used in Server::setup_signal_handlers but only signal is on the subject's allowed list. Not introduced by this PR; flagging for awareness.

Test plan

Integration tests live in tests/phase15_tests.sh (7 cases, all passing). All kanban Phase 1.5 DoD items verified:

  • ./webserv loads config/default.conf and listens on 8080
  • ./webserv config/test_two_servers.conf binds 8090 AND 9090
  • Missing config: clean fatal: cannot open … + exit 1
  • Duplicate listen (test_duplicate.conf): rejected at validation, exit 1, no bind attempted
  • Bad syntax (test_bad_syntax.conf): rejected at parse, exit 1, no crash
  • Junk config: fatal: <line>:<col>: …, exit 1, no segfault
  • argv too many args (./webserv default.conf foo bar): usage + exit 1
  • Bind already in use: fatal: bind failed on 0.0.0.0:8080, exit 1
  • SIGINT during run: clean shutdown, exit 0, both ports freed (lsof confirms)
  • make re clean under -Wall -Wextra -Werror -std=c++98, zero warnings

Run locally:

make re
./tests/phase15_tests.sh

Expected: Phase 1.5: 7 passed, 0 failed.

Known follow-ups (out of scope for this PR)

  • Server::run() swallows POLLHUP|POLLERR on listener fds. Pre-existing pattern from Phase 1.2; with N listeners worth defensive logging + clean shutdown in a Phase 2 polish PR.
  • Linear scan in Server::run()'s accept block to map fd back to the owning Socket*. O(log N) lookup via a sibling map would be cleaner; trivial perf, deferred to Phase 2.4 when the router work touches this code anyway.

Phase 2 readiness

fd_to_server_ is now populated and ready for the request router (Phase 2.4) to dispatch by listener fd. The const Config& borrow is documented with an explicit immutability invariant on the relevant member declaration.

Copilot AI review requested due to automatic review settings May 28, 2026 20:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Replaces the hardcoded single-listener startup with a config-driven flow: main() loads a config file (default config/default.conf or argv[1]), and Server now owns a vector of Socket* listeners — one per ListenSpec — plus a fd → ServerConfig* map for the future router. Socket gains a (host, port) constructor that uses getaddrinfo(AI_NUMERICHOST | AI_PASSIVE), sets SO_REUSEADDR, and follows a strict freeaddrinfo/close cleanup contract on every throw path. Legacy Socket(int) and Server(Socket&) are removed.

Changes:

  • Socket ctor changed to (host, port) with numeric getaddrinfo, SO_REUSEADDR, and resource-cleanup contract.
  • Server ctor now takes const Config&, builds N listeners with partial-init RAII cleanup, and exposes fd_to_server_ for routing.
  • main() adds argv handling, default-path fallback, and a top-level try/catch printing fatal: <msg>; new integration test script + fixture configs added.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/main.cpp New argv/default-path/try-catch driver; dev LEXER_DUMP/CONFIG_DUMP hooks preserved.
includes/socket.hpp Replaces Socket(int) with Socket(const std::string&, int); adds <netdb.h>.
src/socket/socket.cpp Implements new ctor using getaddrinfo, SO_REUSEADDR, non-blocking, bind with cleanup on every throw path.
includes/server.hpp Adds config_, listeners_, fd_to_server_ members and Server(const Config&); removes Socket& member.
src/server/server.cpp New ctor with partial-init RAII; run() and cleanup_sockets() use fd_to_server_ for listener detection; dtor deletes owned listeners.
tests/phase15_tests.sh New bash integration harness covering argv, default path, multi-port, multi-server, duplicate, bad syntax.
config/test_multi_port.conf Fixture: one server with two listen ports.
config/test_two_servers.conf Fixture: two server blocks on distinct ports.
config/test_duplicate.conf Fixture: duplicate listen for validator rejection test.
config/test_bad_syntax.conf Fixture: malformed config for parser error test.
docs/.../phase-1-5-...-design.md Design rationale, scope, and decisions log.
docs/.../phase-1-5-...-plan.md Step-by-step implementation plan with tasks and TDD flow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/server/server.cpp
Comment on lines +58 to 69
} catch (...) {
// Partial-init cleanup: a Socket ctor or startListening() threw.
// listeners_ contains only the ones that fully succeeded; delete
// each (their dtors close their fds).
for (std::size_t k = 0; k < listeners_.size(); ++k)
delete listeners_[k];
listeners_.clear();
fd_to_server_.clear();
throw;
}

setup_signal_handlers();
Comment thread tests/phase15_tests.sh
Comment on lines +107 to +127
# Task 4: default-path happy case — webserv with no arg listens on 8080
./webserv >/tmp/webserv.out 2>&1 &
PID=$!
sleep 1
if ! kill -0 "$PID" 2>/dev/null; then
FAIL=$((FAIL+1)); FAILED_TESTS+=("task4_default_path: webserv exited:
$(cat /tmp/webserv.out)")
else
if lsof -iTCP:8080 -sTCP:LISTEN -P -n 2>/dev/null | grep -q webserv; then
PASS=$((PASS+1))
else
FAIL=$((FAIL+1)); FAILED_TESTS+=("task4_default_path: not listening on 8080")
fi
kill -INT "$PID" 2>/dev/null; wait "$PID" 2>/dev/null
fi

# Task 6: one server, two listen directives
assert_listens_on_ports \
"task6_multi_port_same_server" \
"config/test_multi_port.conf" \
8080 8081
@MukhammadIbrokhimov
MukhammadIbrokhimov force-pushed the feature/phase-1-5-load-config branch from 353d56a to e3510af Compare May 28, 2026 21:04
run() used to walk listeners_ linearly to find which Socket* owned an
incoming POLLIN fd. Cheap with one listener, painful by Phase 4 stress
testing. New parallel map is kept in lock-step with fd_to_server_ in
the ctor, so the existing is_listener check still works unchanged.
@MukhammadIbrokhimov
MukhammadIbrokhimov merged commit 355c323 into main May 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants