Skip to content

Commit 18f980c

Browse files
deckeregonightmechanicRan Katz
authored
Non-Blocking Examples and Tests (#26)
* Added "poll" function/mode (#25) Adapted from the circuitpython webserver Co-authored-by: Ran Katz <rkatz@apple.com> * Merges in code suggestions from @nightmechanic; adds tests; examples for nonblocking mode Co-authored-by: Ran <rankatz@gmail.com> Co-authored-by: Ran Katz <rkatz@apple.com>
1 parent 510d610 commit 18f980c

File tree

9 files changed

+123
-26
lines changed

9 files changed

+123
-26
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,15 @@ pool = socketpool.SocketPool(wifi.radio)
4949
socket = pool.socket()
5050
socket.bind(['0.0.0.0', 80])
5151
socket.listen(1)
52+
socket.setblocking(True)
5253
print("Connected to %s, IPv4 Addr: " % secrets["ssid"], wifi.radio.ipv4_address)
5354

5455
while True:
5556
ampule.listen(socket)
5657
```
5758

5859
The majority of this code is CircuitPython boilerplate that connects to a WiFi
59-
network, listens on port 80, and connects ampule to the open socket.
60+
network, listens on port 80 in blocking mode, and connects ampule to the open socket.
6061

6162
The line `@ampule.route("/hello/world")` registers the following function for
6263
the path specified, and responds with HTTP 200, no headers, and a response body

ampule.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import io
22
import re
33

4+
from errno import EAGAIN, ECONNRESET
5+
46
BUFFER_SIZE = 256
5-
TIMEOUT = 30
67
routes = []
78
variable_re = re.compile("^<([a-zA-Z]+)>$")
89

@@ -43,14 +44,12 @@ def __parse_body(reader):
4344

4445
def __read_request(client):
4546
message = bytearray()
46-
client.settimeout(30)
4747
socket_recv = True
4848

4949
try:
5050
while socket_recv:
5151
buffer = bytearray(BUFFER_SIZE)
5252
client.recv_into(buffer)
53-
start_length = len(message)
5453
for byte in buffer:
5554
if byte == 0x00:
5655
socket_recv = False
@@ -62,7 +61,7 @@ def __read_request(client):
6261

6362
reader = io.BytesIO(message)
6463
line = str(reader.readline(), "utf-8")
65-
(method, full_path, version) = line.rstrip("\r\n").split(None, 2)
64+
(method, full_path, _) = line.rstrip("\r\n").split(None, 2)
6665

6766
request = Request(method, full_path)
6867
request.headers = __parse_headers(reader)
@@ -133,10 +132,16 @@ def __match_route(path, method):
133132
return (match.groups(), route)
134133
return None
135134

136-
def listen(socket, timeout=30):
137-
client, remote_address = socket.accept()
135+
def listen(socket):
136+
try:
137+
client, _ = socket.accept()
138+
except OSError as e:
139+
if e.errno == EAGAIN: return
140+
if e.errno == ECONNRESET: return
141+
print("OS Error with socket:", e)
142+
raise e
143+
138144
try:
139-
client.settimeout(timeout)
140145
request = __read_request(client)
141146
match = __match_route(request.path, request.method)
142147
if match:

examples/digitalio/led.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def light_off(request):
4343
socket = pool.socket()
4444
socket.bind(['0.0.0.0', 80])
4545
socket.listen(1)
46+
socket.setblocking(True)
4647
print("Connected to %s, IPv4 Addr: " % secrets["ssid"], wifi.radio.ipv4_address)
4748

4849
while True:

examples/nonblocking/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Non-blocking Socket Example
2+
3+
This example shows how you could use the HTTP service in non-blocking
4+
mode, allowing you to do other work while waiting for an inbound request
5+
6+
## Examples
7+
8+
To see what the current counter's status is:
9+
10+
```sh
11+
curl -v http://192.168.0.1:7413/status
12+
```

examples/nonblocking/count.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import wifi
2+
import socketpool
3+
import time
4+
import ampule
5+
6+
counter = 0
7+
8+
headers = {
9+
"Content-Type": "application/json; charset=UTF-8",
10+
"Access-Control-Allow-Origin": '*',
11+
"Access-Control-Allow-Methods": 'GET, POST',
12+
"Access-Control-Allow-Headers": 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
13+
}
14+
15+
@ampule.route("/status")
16+
def status(request):
17+
global counter
18+
return (200, headers, '{"counter": %d}' % counter)
19+
20+
try:
21+
from secrets import secrets
22+
except ImportError:
23+
print("WiFi secrets not found in secrets.py")
24+
raise
25+
26+
try:
27+
print("Connecting to %s..." % secrets["ssid"])
28+
print("MAC: ", [hex(i) for i in wifi.radio.mac_address])
29+
wifi.radio.connect(secrets["ssid"], secrets["password"])
30+
except:
31+
print("Error connecting to WiFi")
32+
raise
33+
34+
pool = socketpool.SocketPool(wifi.radio)
35+
socket = pool.socket()
36+
socket.bind(['0.0.0.0', 80])
37+
socket.listen(1)
38+
socket.setblocking(False)
39+
print("Connected to %s, IPv4 Addr: " % secrets["ssid"], wifi.radio.ipv4_address)
40+
41+
while True:
42+
ampule.listen(socket)
43+
counter += 1
44+
time.sleep(1.0)
45+
print("Counter: ", counter)
46+
47+

examples/webserver/webpages.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def demo(request):
8888
socket = pool.socket()
8989
socket.bind(['0.0.0.0', 80])
9090
socket.listen(1)
91+
socket.setblocking(True)
9192
print("Connected to {}, Web server running on http://{}:80".format(secrets["ssid"], wifi.radio.ipv4_address))
9293

9394
while True:

tests/test_body.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,3 @@ def test_body_mime():
3636
ampule.listen(socket)
3737
socket.send.assert_called_once_with(http_helper.expected_response(200, expected_bytes, expected_header))
3838
socket.close.assert_called_once()
39-
40-
def test_body_resend():
41-
body = "Let me tell you a long rambling story child. Once upon a time there was a coaster on my desk that had a picture of a ship. It was blue. Blue like the ocean? No. Blue like a coaster."
42-
socket = mocket.Mocket(("GET /var/get HTTP/1.1\r\n\r\n" + body).encode("utf-8"))
43-
ampule.listen(socket)
44-
expected = [ # Expect two calls, the second after an 256 byte partial data send
45-
mock.call(http_helper.expected_response(200, "GET: "+body)),
46-
mock.call(b"e ocean? No. Blue like a coaster.\r\n")
47-
]
48-
assert socket.send.mock_calls == expected
49-
socket.close.assert_called_once()

tests/test_codes.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
import mocket
2-
from unittest import mock
2+
from unittest import mock, TestCase
33
import ampule
4+
import errno
45
import http_helper
56

6-
def test_socket_error():
7-
socket = mocket.Mocket("GET /bad HTTP/1.1".encode("utf-8"))
8-
socket.settimeout = mock.Mock(side_effect=OSError('SetTimeout'))
9-
ampule.listen(socket)
10-
socket.send.assert_called_once_with(http_helper.expected_response(500, "Error processing request"))
11-
socket.close.assert_called_once()
7+
class MyTestCase(TestCase):
8+
def test_socket_error(self):
9+
socket = mocket.Mocket("GET /bad HTTP/1.1".encode("utf-8"))
10+
socket.accept = mock.Mock(side_effect=OSError('socket failure'))
11+
with self.assertRaises(OSError):
12+
ampule.listen(socket)
13+
socket.send.assert_not_called()
14+
socket.close.assert_not_called()
15+
16+
def test_connection_reset(self):
17+
socket = mocket.Mocket("GET /bad HTTP/1.1".encode("utf-8"))
18+
socket.accept = mock.Mock(side_effect=OSError(errno.ECONNRESET))
19+
socket.send.assert_not_called()
20+
socket.close.assert_not_called()
21+
22+
def test_no_data_currently(self):
23+
socket = mocket.Mocket("GET /bad HTTP/1.1".encode("utf-8"))
24+
socket.accept = mock.Mock(side_effect=OSError(errno.EAGAIN))
25+
socket.send.assert_not_called()
26+
socket.close.assert_not_called()
27+
28+
def test_routing_error(self):
29+
socket = mocket.Mocket("GET /bad HTTP/1.1".encode("utf-8"))
30+
socket.recv_into = mock.Mock(side_effect=OSError('read failure'))
31+
ampule.listen(socket)
32+
socket.send.assert_called_once_with(http_helper.expected_response(500, "Error processing request"))
33+
socket.close.assert_called_once()

tests/test_io.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import mocket
2+
from unittest import mock
3+
import ampule
4+
import http_helper
5+
6+
@ampule.route("/var/get", method='GET')
7+
def trailing_variable(request):
8+
return (200, {}, "GET: %s" % request.body)
9+
10+
def test_body_resend():
11+
body = "Let me tell you a long rambling story child. Once upon a time there was a coaster on my desk that had a picture of a ship. It was blue. Blue like the ocean? No. Blue like a coaster."
12+
socket = mocket.Mocket(("GET /var/get HTTP/1.1\r\n\r\n" + body).encode("utf-8"))
13+
ampule.listen(socket)
14+
expected = [ # Expect two calls, the second after an 256 byte partial data send
15+
mock.call(http_helper.expected_response(200, "GET: "+body)),
16+
mock.call(b"e ocean? No. Blue like a coaster.\r\n")
17+
]
18+
assert socket.send.mock_calls == expected
19+
socket.close.assert_called_once()

0 commit comments

Comments
 (0)