packages feed

network-light 0.1.0.5 → 0.1.0.6

raw patch · 24 files changed

+808/−8 lines, 24 filesdep ~base

Dependency ranges changed: base

Files

CHANGELOG.md view
@@ -23,3 +23,7 @@  * Made it work with GHC again, and added some tests. * Worked on the Haddock documentation, before eventually putting it on Hackage.++## 0.1.0.6 -- 2026-08-06++* Verified that it works with a bunch more GHC cmopilers.
+ README.md view
@@ -0,0 +1,97 @@+# network-light++[![Hackage](https://img.shields.io/hackage/v/network-light.svg)](https://hackage.haskell.org/package/network-light)+[![Apache 2.0 License](<https://img.shields.io/badge/license-Apache%202.0-blue.svg>)](LICENSE)++A small, portable subset of the `network` package's socket API.++## What this is++`network-light` implements a small subset of the socket functionality found in the `network` package: creating TCP/UDP sockets addressable via IPv4, connecting, binding, listening, accepting connections, and sending/receiving raw bytes, `String`, or `ByteString`.+The API deliberately mirrors `network`'s in spirit, so moving between the two should feel familiar.++It exists because [MicroHs](https://github.com/augustss/MicroHs) (`mhs`), a small Haskell compiler, cannot yet compile the full `network` package. `network-light` is implemented directly on top of the C `socket()`/`connect()`/`send()`/`recv()`/... calls via plain FFI imports (no C stubs) which keeps it simple enough to compile under both GHC and MicroHs from the same source. Until MicroHs can compile `network` in full, this package is the quickest way to get sockets working under both compilers.++**This is not, and does not try to be, a replacement for `network`.** It only implements what has been needed so far — `Domain` and `SockOpt`, for example, each model a handful of constructors, not the full POSIX surface.++## Scope++- TCP (`SOCK_STREAM`) and UDP (`SOCK_DGRAM`) sockets over IPv4 (`AF_INET`)+- `connect`, `bind`, `listen`, `accept`, `close`+- Sending and receiving raw buffers, `String`, or `ByteString`, either "best effort"   or looped until the full amount is sent/received+- A handful of socket options: `SO_REUSEADDR`, `SO_DEBUG`, `SO_TYPE`, and   non-blocking mode+- Sockets are non-blocking by default and integrate correctly with both GHC's I/O manager and MicroHs's cooperative, green-thread concurrency+- Compiles under GHC and MicroHs, on Linux; a `zephyr` cabal flag selects the differing `sockaddr_in` layout needed for Zephyr RTOS embedded targets++If you need something this package doesn't have yet, such as IPv6, more socket options, Unix domain sockets, and so on, please fork it, add what you need, and open a pull request. Contributions are very welcome, as long as they keep to the existing style: plain FFI imports, no C stubs unless truly unavoidable, and code that compiles under both GHC and MicroHs.++## Installation++```+cabal install network-light+```++or add it to your `.cabal` file:++```+build-depends: network-light+```++## Example++```haskell+module Main where++import System.Network++port :: Int+port = 4242++server :: IO ()+server = do+    serverFd <- socket AF_INET SOCK_STREAM+    setsocketopt serverFd SO_REUSEADDR 1+    bind serverFd (mkSockAddr port Nothing)+    listen serverFd 1++    (clientFd, clientAddr) <- accept serverFd+    putStrLn ("received connection from: " <> show clientAddr)++    msg <- recvString clientFd 100+    putStrLn msg+    _ <- sendString clientFd "Hello, client!"++    close clientFd+    close serverFd++client :: IO ()+client = do+    fd <- socket AF_INET SOCK_STREAM+    connect fd (mkSockAddr port (Just "127.0.0.1"))++    _ <- sendString fd "Hello, server!"+    reply <- recvString fd 100+    putStrLn reply++    close fd++-- Run `server` in one terminal and `client` in another; they will talk to each other.+main :: IO ()+main = server+```++## Testing++The test suite lives under `tests/` and is driven by `make` rather than+`cabal test`, so that every test is built and run against both GHC and MicroHs from the same source:++```+cd tests+make test          # run every test under both mhs and ghc+make test HC=ghc   # GHC only+make test HC=mhs   # MicroHs only+```++## License++Apache License 2.0. See [LICENSE](LICENSE).
+ examples/Client.hs view
@@ -0,0 +1,18 @@+module Client where+import System.Network+import System.Environment+import Secret++main :: IO ()+main = do+  args <- getArgs+  let msg | null args = "Hello"+          | otherwise = unwords args+  fd <- socket AF_INET SOCK_STREAM+  connect fd (mkSockAddr 9900 (Just "127.0.0.1"))+  sendString fd secret+  putStrLn $ "Sending: " ++ msg+  sendString fd msg+  str <- recvString fd 1000+  putStrLn $ "Got: " ++ show str+  close fd
+ examples/Secret.hs view
@@ -0,0 +1,3 @@+module Secret where+secret :: String+secret = "Secret"
+ examples/Server.hs view
@@ -0,0 +1,27 @@+module Server where+import System.Network+import Secret++main :: IO ()+main = do+  sfd <- socket AF_INET SOCK_STREAM+  setsocketopt sfd SO_REUSEADDR 1+  bind sfd (mkSockAddr 9900 Nothing)+  listen sfd 2+  serve sfd++serve :: Socket -> IO ()+serve sfd = do+  putStrLn "accepting"+  (fd, addr) <- accept sfd+  putStrLn $ "got connection " ++ show addr+  sec <- recvString fd (length secret)+  if (sec == secret) then do+    str <- recvString fd 1000+    putStrLn $ "echoing"+    sendString fd (reverse str)+    return ()+   else do+    putStrLn "bad secret"+  close fd+  serve sfd
network-light.cabal view
@@ -1,8 +1,8 @@ cabal-version:      3.0 name:               network-light-version:            0.1.0.5+version:            0.1.0.6 synopsis:           A slimmed down version of network-description:        A slimmed down version of network, that works with both GHC and MHS. Very incomplete -- pull-requests welcome.+description:        A slimmed down version of network, that works with both GHC and MHS. license:            Apache-2.0 license-file:       LICENSE author:             Robert Krook@@ -11,12 +11,29 @@ category:           Network build-type:         Simple extra-doc-files:    CHANGELOG.md+                    README.md+extra-source-files: examples/*.hs+                    tests/Makefile+                    tests/unittests/**/*.hs+                    tests/unittests/bash-pipes/Makefile+                    tests/unittests/concurrent-clients/Makefile+                    tests/unittests/connect-closed-port/Makefile+                    tests/unittests/hello-world/Makefile+                    tests/unittests/send-string/Makefile+                    tests/unittests/so-reuseaddr-churn/Makefile bug-reports:        https://github.com/Rewbert/network-light/issues-tested-with:        GHC ==9.10.3+tested-with:        GHC ==9.4.8+                     || ==9.6.7+                     || ==9.8.1+                     || ==9.8.2+                     || ==9.8.4+                     || ==9.10.1+                     || ==9.10.2+                     || ==9.10.3                      || ==9.12.2-                     || ==9.14.1+                     || ==9.12.4+                     || ==9.14.1,                     MHS ==0.16.4.0--- extra-source-files:  source-repository head   type:     git@@ -37,13 +54,14 @@     -- other-extensions:     if flag(zephyr)         cpp-options: -DZEPHYR-    build-depends:    base >=4.20 && <5, bytestring >=0.12.2.0 && <0.13+    build-depends:    base >=4.17 && <5, bytestring >=0.12.2.0 && <0.13     hs-source-dirs:   src     default-language: Haskell2010  Test-Suite test-sockaddr-storable     type: exitcode-stdio-1.0     Default-language: Haskell2010-    hs-source-dirs: test/Network+    hs-source-dirs: test/Network, src     main-is: SockAddr.hs-    build-depends: base, network-light, QuickCheck+    other-modules: System.Network.Types+    build-depends: base >=4.17 && <5, network-light, QuickCheck
+ tests/Makefile view
@@ -0,0 +1,42 @@+# Runs every test under unittests/ with both compilers, so a change can+# be checked against cabal/ghc and mcabal/mhs in one shot.+#+# Usage:+#   make test              # run every test under both mhs and ghc+#   make test HC=mhs       # run every test under mhs only+#   make test HC=ghc       # run every test under ghc only+#   make clean             # clean every test directory++.PHONY: all test clean++UNITTESTS := $(sort $(patsubst %/,%,$(dir $(wildcard unittests/*/Makefile))))++ifeq ($(HC),)+HCS := mhs ghc+else+HCS := $(HC)+endif++all: test++test:+	@fail=0; \+	total=0; \+	for d in $(UNITTESTS); do \+		for hc in $(HCS); do \+			total=$$((total+1)); \+			$(MAKE) -s -C $$d clean >/dev/null 2>&1; \+			if ! $(MAKE) -s -C $$d test HC=$$hc; then fail=$$((fail+1)); fi; \+			$(MAKE) -s -C $$d clean >/dev/null 2>&1; \+		done; \+	done; \+	echo "-----"; \+	if [ $$fail -eq 0 ]; then \+		echo "all $$total test runs passed"; \+	else \+		echo "$$fail of $$total test runs FAILED"; \+	fi; \+	exit $$fail++clean:+	@for d in $(UNITTESTS); do $(MAKE) -s -C $$d clean; done
+ tests/unittests/bash-pipes/Client.hs view
@@ -0,0 +1,21 @@+module Client where++import System.IO+import System.Network++port :: Int+port = 9935++main :: IO ()+main = do+    input <- getContents++    fd <- socket AF_INET SOCK_STREAM+    connect fd (mkSockAddr port (Just "127.0.0.1"))++    sendString fd input+    reply <- recvString fd 65536+    close fd++    putStr reply+    hFlush stdout
+ tests/unittests/bash-pipes/Makefile view
@@ -0,0 +1,55 @@+# Checks that a network-light client can sit inside a normal bash+# pipeline: stdin is piped in from a real shell command, the client+# forwards that over a socket to a network-light server, the server's+# reply comes back out the client's stdout, and a further bash stage+# consumes it. Exercises the library's sockets together with mhs/GHC's+# ordinary stdin/stdout handling in the same process, not just the+# toolchain in isolation.+#+# Usage:+#   make test              # build and run with mhs (default)+#   make test HC=ghc       # build and run with GHC, against the library source+#   make clean++.PHONY: all test clean++HC     ?= mhs+TOPSRC := ../../../src++all: test++ifeq ($(HC),mhs)+server: Server.hs+	mhs -i$(TOPSRC) Server -o server+client: Client.hs+	mhs -i$(TOPSRC) Client -o client+else ifeq ($(HC),ghc)+server: Server.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Server Server.hs -o server+client: Client.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Client Client.hs -o client+else+server client:+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')+endif++test: server client+	@rm -f server.out+	@( timeout 10 ./server > server.out 2>&1 & )+	@i=0; \+	while [ $$i -lt 50 ] && ! grep -q READY server.out 2>/dev/null; do sleep 0.1; i=$$((i+1)); done; \+	if ! grep -q READY server.out 2>/dev/null; then \+		echo "FAIL: bash-pipes ($(HC)): server never became ready"; \+		cat server.out; exit 1; \+	fi+	@got=$$(printf "banana\napple\ncherry\n" | ./client | grep A | wc -l); \+	if [ "$$got" = "2" ]; then \+		echo "PASS: bash-pipes ($(HC))"; \+	else \+		echo "FAIL: bash-pipes ($(HC)): expected 2, got '$$got'"; \+		cat server.out; \+		exit 1; \+	fi++clean:+	rm -rf server client dist-ghc *.hi *.o server.out
+ tests/unittests/bash-pipes/Server.hs view
@@ -0,0 +1,25 @@+module Server where++import Data.Char+import System.IO+import System.Network++port :: Int+port = 9935++main :: IO ()+main = do+    sfd <- socket AF_INET SOCK_STREAM+    setsocketopt sfd SO_REUSEADDR 1+    bind sfd (mkSockAddr port Nothing)+    listen sfd 1++    putStrLn "READY"+    hFlush stdout++    (cfd, _) <- accept sfd+    msg <- recvString cfd 65536+    hPutStrLn stderr ("SERVER_GOT:" ++ show (length msg) ++ " bytes")+    sendString cfd (map toUpper msg)+    close cfd+    close sfd
+ tests/unittests/concurrent-clients/Client.hs view
@@ -0,0 +1,23 @@+module Client where++import System.Environment+import System.IO+import System.Network++port :: Int+port = 9932++main :: IO ()+main = do+    [idx] <- getArgs+    let msg = "client-" ++ idx++    fd <- socket AF_INET SOCK_STREAM+    connect fd (mkSockAddr port (Just "127.0.0.1"))++    sendString fd msg+    reply <- recvString fd 1024+    putStrLn ("CLIENT_" ++ idx ++ "_GOT:" ++ reply)+    hFlush stdout++    close fd
+ tests/unittests/concurrent-clients/Makefile view
@@ -0,0 +1,64 @@+# Starts one server and N clients launched at (roughly) the same time,+# and checks every client got the reply meant for it. Concurrency here+# is at the OS-process level (N separate client binaries backgrounded+# from the shell) rather than forkIO inside one process -- see+# so-reuseaddr-churn/Makefile for why that matters under GHC.+#+# Usage:+#   make test              # build and run with mhs (default)+#   make test HC=ghc       # build and run with GHC, against the library source+#   make clean++.PHONY: all test clean++HC       ?= mhs+TOPSRC   := ../../../src+NCLIENTS := 8++all: test++ifeq ($(HC),mhs)+server: Server.hs+	mhs -i$(TOPSRC) Server -o server+client: Client.hs+	mhs -i$(TOPSRC) Client -o client+else ifeq ($(HC),ghc)+server: Server.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Server Server.hs -o server+client: Client.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Client Client.hs -o client+else+server client:+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')+endif++test: server client+	@rm -f server.out client-*.out+	@( timeout 20 ./server $(NCLIENTS) > server.out 2>&1 & )+	@i=0; \+	while [ $$i -lt 50 ] && ! grep -q READY server.out 2>/dev/null; do sleep 0.1; i=$$((i+1)); done; \+	if ! grep -q READY server.out 2>/dev/null; then \+		echo "FAIL: concurrent-clients ($(HC)): server never became ready"; \+		cat server.out; exit 1; \+	fi+	@for i in $$(seq 1 $(NCLIENTS)); do \+		./client $$i > client-$$i.out 2>&1 & \+	done; \+	wait+	@sleep 0.3+	@ok=1; \+	for i in $$(seq 1 $(NCLIENTS)); do \+		grep -q "SERVER_GOT:client-$$i$$" server.out || { echo "FAIL: server never saw client-$$i"; ok=0; }; \+		grep -q "CLIENT_$${i}_GOT:ack:client-$$i$$" client-$$i.out || { echo "FAIL: client $$i got the wrong (or no) reply"; ok=0; }; \+	done; \+	grep -q SERVER_DONE server.out || { echo "FAIL: server did not finish"; ok=0; }; \+	if [ $$ok -eq 1 ]; then \+		echo "PASS: concurrent-clients ($(HC))"; \+	else \+		echo "--- server.out ---"; cat server.out; \+		for i in $$(seq 1 $(NCLIENTS)); do echo "--- client-$$i.out ---"; cat client-$$i.out; done; \+		exit 1; \+	fi++clean:+	rm -rf server client dist-ghc *.hi *.o server.out client-*.out
+ tests/unittests/concurrent-clients/Server.hs view
@@ -0,0 +1,40 @@+module Server where++import System.Environment+import System.IO+import System.Network++port :: Int+port = 9932++serve :: Socket -> IO ()+serve sfd = do+    (cfd, _) <- accept sfd+    msg <- recvString cfd 1024+    putStrLn ("SERVER_GOT:" ++ msg)+    hFlush stdout+    sendString cfd ("ack:" ++ msg)+    close cfd++main :: IO ()+main = do+    [nStr] <- getArgs+    let n = read nStr :: Int++    sfd <- socket AF_INET SOCK_STREAM+    setsocketopt sfd SO_REUSEADDR 1+    bind sfd (mkSockAddr port Nothing)+    listen sfd n++    putStrLn "READY"+    hFlush stdout++    -- Clients connect concurrently (the OS queues them in the listen+    -- backlog); the server itself just drains that backlog one at a+    -- time, which is enough to prove several clients can be in flight+    -- against the same listening socket at once.+    mapM_ (const (serve sfd)) [1 .. n]++    close sfd+    putStrLn "SERVER_DONE"+    hFlush stdout
+ tests/unittests/connect-closed-port/ConnectSafe.hs view
@@ -0,0 +1,13 @@+module ConnectSafe where++import System.Network++-- Nothing listens here; connect' should return False instead of throwing.+port :: Int+port = 9934++main :: IO ()+main = do+    fd <- socket AF_INET SOCK_STREAM+    ok <- connect' fd (mkSockAddr port (Just "127.0.0.1"))+    putStrLn ("CONNECT_RESULT:" ++ show ok)
+ tests/unittests/connect-closed-port/ConnectThrows.hs view
@@ -0,0 +1,13 @@+module ConnectThrows where++import System.Network++-- Nothing listens here; connecting should raise an IO exception.+port :: Int+port = 9934++main :: IO ()+main = do+    fd <- socket AF_INET SOCK_STREAM+    connect fd (mkSockAddr port (Just "127.0.0.1"))+    putStrLn "UNEXPECTED_SUCCESS"
+ tests/unittests/connect-closed-port/Makefile view
@@ -0,0 +1,45 @@+# Checks the two failure-reporting conventions the library offers for a+# connect that can't succeed: `connect` throws, `connect'` returns False.+#+# Usage:+#   make test              # build and run with mhs (default)+#   make test HC=ghc       # build and run with GHC, against the library source+#   make clean++.PHONY: all test clean++HC     ?= mhs+TOPSRC := ../../../src++all: test++ifeq ($(HC),mhs)+throws: ConnectThrows.hs+	mhs -i$(TOPSRC) ConnectThrows -o throws+safe: ConnectSafe.hs+	mhs -i$(TOPSRC) ConnectSafe -o safe+else ifeq ($(HC),ghc)+throws: ConnectThrows.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is ConnectThrows ConnectThrows.hs -o throws+safe: ConnectSafe.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is ConnectSafe ConnectSafe.hs -o safe+else+throws safe:+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')+endif++test: throws safe+	@./throws > throws.out 2>&1; rc=$$?; \+	if [ $$rc -eq 0 ]; then \+		echo "FAIL: connect-closed-port ($(HC)): connect to a closed port did not fail"; \+		cat throws.out; exit 1; \+	fi+	@./safe > safe.out 2>&1; rc=$$?; \+	if [ $$rc -ne 0 ] || ! grep -q "CONNECT_RESULT:False" safe.out; then \+		echo "FAIL: connect-closed-port ($(HC)): connect' did not cleanly return False"; \+		cat safe.out; exit 1; \+	fi+	@echo "PASS: connect-closed-port ($(HC))"++clean:+	rm -rf throws safe dist-ghc *.hi *.o *.out
+ tests/unittests/hello-world/Client.hs view
@@ -0,0 +1,15 @@+module Client where++import System.Network++port :: Int+port = 9936++main :: IO ()+main = do+    fd <- socket AF_INET SOCK_STREAM+    connect fd (mkSockAddr port (Just "127.0.0.1"))++    sendString fd "hello world"++    close fd
+ tests/unittests/hello-world/Makefile view
@@ -0,0 +1,48 @@+# Minimal integration test: a server accepts one connection and prints+# whatever the client sent. No handshake protocol between the two --+# just a short sleep to give the server time to start listening before+# the client connects.+#+# Usage:+#   make test              # build and run with mhs (default)+#   make test HC=ghc       # build and run with GHC, against the library source+#   make clean++.PHONY: all test clean++HC     ?= mhs+TOPSRC := ../../../src++all: test++ifeq ($(HC),mhs)+server: Server.hs+	mhs -i$(TOPSRC) Server -o server+client: Client.hs+	mhs -i$(TOPSRC) Client -o client+else ifeq ($(HC),ghc)+server: Server.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Server Server.hs -o server+client: Client.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Client Client.hs -o client+else+server client:+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')+endif++test: server client+	@rm -f server.out+	@( timeout 5 ./server > server.out 2>&1 & )+	@sleep 0.2+	@./client+	@sleep 0.2+	@if grep -q "hello world" server.out; then \+		echo "PASS: hello-world ($(HC))"; \+	else \+		echo "FAIL: hello-world ($(HC))"; \+		cat server.out; \+		exit 1; \+	fi++clean:+	rm -rf server client dist-ghc *.hi *.o server.out
+ tests/unittests/hello-world/Server.hs view
@@ -0,0 +1,20 @@+module Server where++import System.Network++port :: Int+port = 9936++main :: IO ()+main = do+    sfd <- socket AF_INET SOCK_STREAM+    setsocketopt sfd SO_REUSEADDR 1+    bind sfd (mkSockAddr port Nothing)+    listen sfd 1++    (cfd, _) <- accept sfd+    msg <- recvString cfd 1024+    putStrLn msg++    close cfd+    close sfd
+ tests/unittests/send-string/Client.hs view
@@ -0,0 +1,22 @@+module Client where++import System.IO+import System.Network++port :: Int+port = 9931++message :: String+message = "hello-network-light"++main :: IO ()+main = do+    fd <- socket AF_INET SOCK_STREAM+    connect fd (mkSockAddr port (Just "127.0.0.1"))++    sendString fd message+    reply <- recvString fd 1024+    putStrLn ("CLIENT_GOT:" ++ reply)+    hFlush stdout++    close fd
+ tests/unittests/send-string/Makefile view
@@ -0,0 +1,62 @@+# Integration test: a server accepts one connection, echoes back an+# acknowledgement of whatever string the client sent, and the harness+# checks that both sides saw the expected bytes.+#+# Usage:+#   make test              # build and run with mhs (default)+#   make test HC=ghc       # build and run with GHC, against the library source+#   make clean++.PHONY: all test clean++HC     ?= mhs+TOPSRC := ../../../src++EXPECT_SERVER := SERVER_GOT:hello-network-light+EXPECT_CLIENT := CLIENT_GOT:ack:hello-network-light++all: test++ifeq ($(HC),mhs)+# -i points straight at the in-tree library source, so the test always+# exercises the current checkout instead of whatever network-light build+# last happened to be installed into the shared mcabal package cache.+server: Server.hs+	mhs -i$(TOPSRC) Server -o server+client: Client.hs+	mhs -i$(TOPSRC) Client -o client+else ifeq ($(HC),ghc)+server: Server.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Server Server.hs -o server+client: Client.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Client Client.hs -o client+else+server client:+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')+endif++test: server client+	@rm -f server.out client.out+	@( timeout 5 ./server > server.out 2>&1 & )+	@i=0; \+	while [ $$i -lt 50 ] && ! grep -q READY server.out 2>/dev/null; do \+		sleep 0.1; i=$$((i+1)); \+	done; \+	if ! grep -q READY server.out 2>/dev/null; then \+		echo "FAIL: send-string ($(HC)): server never became ready"; \+		cat server.out; \+		exit 1; \+	fi+	@./client > client.out 2>&1+	@sleep 0.2+	@if grep -q "$(EXPECT_SERVER)" server.out && grep -q "$(EXPECT_CLIENT)" client.out; then \+		echo "PASS: send-string ($(HC))"; \+	else \+		echo "FAIL: send-string ($(HC))"; \+		echo "--- server.out ---"; cat server.out; \+		echo "--- client.out ---"; cat client.out; \+		exit 1; \+	fi++clean:+	rm -rf server client dist-ghc *.hi *.o server.out client.out
+ tests/unittests/send-string/Server.hs view
@@ -0,0 +1,27 @@+module Server where++import System.IO+import System.Network++port :: Int+port = 9931++main :: IO ()+main = do+    sfd <- socket AF_INET SOCK_STREAM+    setsocketopt sfd SO_REUSEADDR 1+    bind sfd (mkSockAddr port Nothing)+    listen sfd 1++    -- Tell the test harness we're ready to accept a connection.+    putStrLn "READY"+    hFlush stdout++    (cfd, _) <- accept sfd+    msg <- recvString cfd 1024+    putStrLn ("SERVER_GOT:" ++ msg)+    hFlush stdout++    sendString cfd ("ack:" ++ msg)+    close cfd+    close sfd
+ tests/unittests/so-reuseaddr-churn/Makefile view
@@ -0,0 +1,64 @@+# Rebinds the same port many times in a row, each time via a fresh+# socket, right after actively closing an accepted connection on it.+# Without SO_REUSEADDR this fails with EADDRINUSE once TIME_WAIT sockets+# pile up; with it, it should sail through every iteration.+#+# Deliberately drives the connections from plain bash (/dev/tcp) rather+# than another Haskell process, so this test doesn't depend on+# concurrency working inside the compiled binary (see the note in+# concurrent-clients/ and send-string/'s README about forkIO deadlocking+# blocking FFI calls under GHC).+#+# Usage:+#   make test              # build and run with mhs (default)+#   make test HC=ghc       # build and run with GHC, against the library source+#   make clean++.PHONY: all test clean++SHELL  := /bin/bash+HC     ?= mhs+TOPSRC := ../../../src+PORT   := 9933+ITERS  := 20++all: test++ifeq ($(HC),mhs)+server: Server.hs+	mhs -i$(TOPSRC) Server -o server+else ifeq ($(HC),ghc)+server: Server.hs+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Server Server.hs -o server+else+server:+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')+endif++test: server+	@rm -f server.out+	@( timeout 20 ./server $(ITERS) > server.out 2>&1 & )+	@for i in $$(seq 1 $(ITERS)); do \+		j=0; \+		while [ $$j -lt 50 ] && ! grep -q "READY_$$i$$" server.out 2>/dev/null; do \+			sleep 0.05; j=$$((j+1)); \+		done; \+		if ! grep -q "READY_$$i$$" server.out 2>/dev/null; then \+			echo "FAIL: so-reuseaddr-churn ($(HC)): server never reached iteration $$i"; \+			cat server.out; exit 1; \+		fi; \+		bash -c 'exec 3<>/dev/tcp/127.0.0.1/$(PORT); sleep 0.2' 2>/dev/null || { \+			echo "FAIL: so-reuseaddr-churn ($(HC)): could not connect on iteration $$i"; \+			cat server.out; exit 1; \+		}; \+	done+	@sleep 0.3+	@if grep -q SERVER_DONE server.out; then \+		echo "PASS: so-reuseaddr-churn ($(HC))"; \+	else \+		echo "FAIL: so-reuseaddr-churn ($(HC)): server did not complete all iterations"; \+		cat server.out; exit 1; \+	fi++clean:+	rm -rf server dist-ghc *.hi *.o server.out
+ tests/unittests/so-reuseaddr-churn/Server.hs view
@@ -0,0 +1,34 @@+module Server where++import System.Environment+import System.IO+import System.Network++port :: Int+port = 9933++-- One churn cycle: bind a fresh socket to the same port, accept exactly+-- one connection, and actively close it from our end (the side that+-- closes first is the side that lands in TIME_WAIT). Without+-- SO_REUSEADDR the next iteration's bind fails with EADDRINUSE while+-- that TIME_WAIT entry is still around.+churn :: Int -> IO ()+churn i = do+    sfd <- socket AF_INET SOCK_STREAM+    setsocketopt sfd SO_REUSEADDR 1+    bind sfd (mkSockAddr port Nothing)+    listen sfd 1++    putStrLn ("READY_" ++ show i)+    hFlush stdout++    (afd, _) <- accept sfd+    close afd+    close sfd++main :: IO ()+main = do+    [nStr] <- getArgs+    mapM_ churn [1 .. (read nStr :: Int)]+    putStrLn "SERVER_DONE"+    hFlush stdout