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 +4/−0
- README.md +97/−0
- examples/Client.hs +18/−0
- examples/Secret.hs +3/−0
- examples/Server.hs +27/−0
- network-light.cabal +26/−8
- tests/Makefile +42/−0
- tests/unittests/bash-pipes/Client.hs +21/−0
- tests/unittests/bash-pipes/Makefile +55/−0
- tests/unittests/bash-pipes/Server.hs +25/−0
- tests/unittests/concurrent-clients/Client.hs +23/−0
- tests/unittests/concurrent-clients/Makefile +64/−0
- tests/unittests/concurrent-clients/Server.hs +40/−0
- tests/unittests/connect-closed-port/ConnectSafe.hs +13/−0
- tests/unittests/connect-closed-port/ConnectThrows.hs +13/−0
- tests/unittests/connect-closed-port/Makefile +45/−0
- tests/unittests/hello-world/Client.hs +15/−0
- tests/unittests/hello-world/Makefile +48/−0
- tests/unittests/hello-world/Server.hs +20/−0
- tests/unittests/send-string/Client.hs +22/−0
- tests/unittests/send-string/Makefile +62/−0
- tests/unittests/send-string/Server.hs +27/−0
- tests/unittests/so-reuseaddr-churn/Makefile +64/−0
- tests/unittests/so-reuseaddr-churn/Server.hs +34/−0
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++[](https://hackage.haskell.org/package/network-light)+[](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