5 changed files with 0 additions and 541 deletions
@ -1,45 +0,0 @@ |
|||||
-- A generic fifo module. See docs/lua-modules/fifo.md for use examples. |
|
||||
|
|
||||
local tr, ti = table.remove, table.insert |
|
||||
|
|
||||
-- Remove an element and pass it to k, together with a boolean indicating that |
|
||||
-- this is the last element in the queue; if that returns a value, leave that |
|
||||
-- pending at the top of the fifo. |
|
||||
-- |
|
||||
-- If k returns nil, the fifo will be advanced. Moreover, k may return a |
|
||||
-- second result, a boolean, indicating "phantasmic" nature of this element. |
|
||||
-- If this boolean is true, then the fifo will advance again, passing the next |
|
||||
-- value, if there is one, to k, or priming itself for immediate execution at |
|
||||
-- the next call to queue. |
|
||||
-- |
|
||||
-- If the queue is empty, do not invoke k but flag it to enable immediate |
|
||||
-- execution at the next call to queue. |
|
||||
-- |
|
||||
-- Returns 'true' if the queue contained at least one non-phantom entry, |
|
||||
-- 'false' otherwise. |
|
||||
local function dequeue(q,k) |
|
||||
if #q > 0 |
|
||||
then |
|
||||
local new, again = k(q[1], #q == 1) |
|
||||
if new == nil |
|
||||
then tr(q,1) |
|
||||
if again then return dequeue(q, k) end -- note tail call |
|
||||
else q[1] = new |
|
||||
end |
|
||||
return true |
|
||||
else q._go = true ; return false |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
-- Queue a on queue q and dequeue with `k` if the fifo had previously emptied. |
|
||||
local function queue(q,a,k) |
|
||||
ti(q,a) |
|
||||
if k ~= nil and q._go then q._go = false; dequeue(q, k) end |
|
||||
end |
|
||||
|
|
||||
-- return a table containing just the FIFO constructor |
|
||||
return { |
|
||||
['new'] = function() |
|
||||
return { ['_go'] = true ; ['queue'] = queue ; ['dequeue'] = dequeue } |
|
||||
end |
|
||||
} |
|
@ -1,134 +0,0 @@ |
|||||
-- Wrap a two-staged fifo around a socket's send; see |
|
||||
-- docs/lua-modules/fifosock.lua for more documentation. |
|
||||
-- |
|
||||
-- See fifosocktest.lua for some examples of use or tricky cases. |
|
||||
-- |
|
||||
-- Our fifos can take functions; these can be useful for either lazy |
|
||||
-- generators or callbacks for parts of the stream having been sent. |
|
||||
|
|
||||
local BIGTHRESH = 256 -- how big is a "big" string? |
|
||||
local SPLITSLOP = 16 -- any slop in the big question? |
|
||||
local FSMALLLIM = 32 -- maximum number of small strings held |
|
||||
local COALIMIT = 3 |
|
||||
|
|
||||
local concat = table.concat |
|
||||
local insert = table.insert |
|
||||
local gc = collectgarbage |
|
||||
|
|
||||
local function wrap(sock) |
|
||||
-- the two fifos |
|
||||
local fsmall, lsmall, fbig = {}, 0, (require "fifo").new() |
|
||||
|
|
||||
-- ssend last aggregation string and aggregate count |
|
||||
local ssla, sslan = nil, 0 |
|
||||
local ssend = function(s,islast) |
|
||||
local ns = nil |
|
||||
|
|
||||
-- Optimistically, try coalescing FIFO dequeues. But, don't try to |
|
||||
-- coalesce function outputs, since functions might be staging their |
|
||||
-- execution on the send event implied by being called. |
|
||||
|
|
||||
if type(s) == "function" then |
|
||||
if sslan ~= 0 then |
|
||||
sock:send(ssla) |
|
||||
ssla, sslan = nil, 0; gc() |
|
||||
return s, false -- stay as is and wait for :on("sent") |
|
||||
end |
|
||||
s, ns = s() |
|
||||
elseif type(s) == "string" and sslan < COALIMIT then |
|
||||
if sslan == 0 |
|
||||
then ssla, sslan = s, 1 |
|
||||
else ssla, sslan = ssla .. s, sslan + 1 |
|
||||
end |
|
||||
if islast then |
|
||||
-- this is shipping; if there's room, steal the small fifo, too |
|
||||
if sslan < COALIMIT then |
|
||||
sock:send(ssla .. concat(fsmall)) |
|
||||
fsmall, lsmall = {}, 0 |
|
||||
else |
|
||||
sock:send(ssla) |
|
||||
end |
|
||||
ssla, sslan = "", 0; gc() |
|
||||
return nil, false |
|
||||
else |
|
||||
return nil, true |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
-- Either that was a function or we've hit our coalescing limit or |
|
||||
-- we didn't ship above. Ship now, if there's something to ship. |
|
||||
if s ~= nil then |
|
||||
if sslan == 0 then sock:send(s) else sock:send(ssla .. s) end |
|
||||
ssla, sslan = nil, 0; gc() |
|
||||
return ns or nil, false |
|
||||
elseif sslan ~= 0 then |
|
||||
assert (ns == nil) |
|
||||
sock:send(ssla) |
|
||||
ssla, sslan = nil, 0; gc() |
|
||||
return nil, false |
|
||||
else |
|
||||
assert (ns == nil) |
|
||||
return nil, true |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
-- Move fsmall to fbig; might send if fbig empty |
|
||||
local function promote(f) |
|
||||
if #fsmall == 0 then return end |
|
||||
local str = concat(fsmall) |
|
||||
fsmall, lsmall = {}, 0 |
|
||||
fbig:queue(str, f or ssend) |
|
||||
end |
|
||||
|
|
||||
local function sendnext() |
|
||||
if not fbig:dequeue(ssend) then promote() end |
|
||||
end |
|
||||
|
|
||||
sock:on("sent", sendnext) |
|
||||
|
|
||||
return function(s) |
|
||||
-- don't sweat the petty things |
|
||||
if s == nil or s == "" then return end |
|
||||
|
|
||||
-- Function? Go ahead and queue this thing in the right place. |
|
||||
if type(s) == "function" then promote(); fbig:queue(s, ssend); return; end |
|
||||
|
|
||||
s = tostring(s) |
|
||||
|
|
||||
-- cork sending until the end in case we're the head of line |
|
||||
local corked = false |
|
||||
local function corker(t) corked = true; return t end |
|
||||
|
|
||||
-- small fifo would overfill? promote it |
|
||||
if lsmall + #s > BIGTHRESH or #fsmall >= FSMALLLIM then promote(corker) end |
|
||||
|
|
||||
-- big string? chunk and queue big components immediately |
|
||||
-- behind any promotion that just took place |
|
||||
while #s > BIGTHRESH + SPLITSLOP do |
|
||||
local pfx |
|
||||
pfx, s = s:sub(1,BIGTHRESH), s:sub(BIGTHRESH+1) |
|
||||
fbig:queue(pfx, corker) |
|
||||
end |
|
||||
|
|
||||
-- Big string? queue and maybe tx now |
|
||||
if #s > BIGTHRESH then fbig:queue(s, corker) |
|
||||
-- small and fifo in immediate dequeue mode |
|
||||
elseif fbig._go and lsmall == 0 then fbig:queue(s, corker) |
|
||||
-- small and queue already moving; let it linger in the small fifo |
|
||||
else insert(fsmall, s) ; lsmall = lsmall + #s |
|
||||
end |
|
||||
|
|
||||
-- if it happened that we corked the transmission above... |
|
||||
-- if we queued a good amount of data, go ahead and start transmitting; |
|
||||
-- otherwise, wait a tick and hopefully we will queue more in the interim |
|
||||
-- before transmitting. |
|
||||
if corked then |
|
||||
if #fbig <= COALIMIT |
|
||||
then tmr.create():alarm(1, tmr.ALARM_SINGLE, sendnext) |
|
||||
else sendnext() |
|
||||
end |
|
||||
end |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
return { wrap = wrap } |
|
@ -1,145 +0,0 @@ |
|||||
-- |
|
||||
-- Set verbose to 0 for quiet output (either the first assertion failure or |
|
||||
-- "All tests OK"), to 1 to see the events ("SEND", "SENT", "CHECK") without |
|
||||
-- the actual bytes, or to 2 to see the events with the bytes. |
|
||||
-- |
|
||||
local verbose = 0 |
|
||||
|
|
||||
local vprint = (verbose > 0) and print or function() end |
|
||||
|
|
||||
-- |
|
||||
-- Mock up enough of the nodemcu tmr structure, but pretend that nothing |
|
||||
-- happens between ticks. This won't exercise the optimistic corking logic, |
|
||||
-- but that's probably fine. |
|
||||
-- luacheck: push ignore |
|
||||
tmr = {} |
|
||||
tmr.ALARM_SINGLE = 0 |
|
||||
function tmr.create() |
|
||||
local r = {} |
|
||||
function r:alarm(_i, _t, cb) vprint("TMR") cb() end |
|
||||
return r |
|
||||
end |
|
||||
-- luacheck: pop |
|
||||
|
|
||||
-- |
|
||||
-- Mock up enough of the nodemcu net.socket type; have it log all the sends |
|
||||
-- into this "outs" array so that we can later check against it. |
|
||||
-- |
|
||||
local outs = {} |
|
||||
local fakesock = { |
|
||||
cb = nil, |
|
||||
on = function(this, _, cb) this.cb = cb end, |
|
||||
send = function(this, s) vprint("SEND", (verbose > 1) and s) table.insert(outs, s) end -- luacheck: no unused |
|
||||
} |
|
||||
local function sent() vprint("SENT") fakesock.cb() end |
|
||||
|
|
||||
-- And wrap a fifosock around this fake socket |
|
||||
local fsend = (require "fifosock").wrap(fakesock) |
|
||||
|
|
||||
-- Verify that the next unconsumed output is as indicated |
|
||||
local function fcheck(x) |
|
||||
vprint ("CHECK", (verbose > 1) and x) |
|
||||
assert (#outs > 0) |
|
||||
assert (x == outs[1]) |
|
||||
table.remove(outs, 1) |
|
||||
end |
|
||||
|
|
||||
-- Enqueue an empty function to prevent coalescing within the fifosock |
|
||||
local function nocoal() fsend(function() return nil end) end |
|
||||
|
|
||||
-- Send and check, for when the string should be sent exactly as is |
|
||||
local function fsendc(x) fsend(x) fcheck(x) end |
|
||||
|
|
||||
-- Check that there are no more outputs |
|
||||
local function fchecke() vprint("CHECKE") assert (#outs == 0) end |
|
||||
|
|
||||
-- |
|
||||
-- And now for the tests, which start easy and grow in complexity |
|
||||
-- |
|
||||
|
|
||||
fsendc("abracadabra none") |
|
||||
sent() ; fchecke() |
|
||||
|
|
||||
fsendc("abracadabra three") |
|
||||
fsend("short") |
|
||||
fsend("string") |
|
||||
fsend("build") |
|
||||
sent() ; fcheck("shortstringbuild") |
|
||||
sent() ; fchecke() |
|
||||
|
|
||||
-- Hit default FSMALLLIM while building up |
|
||||
fsendc("abracadabra lots small") |
|
||||
for i = 1, 32 do fsend("a") end -- luacheck: no unused |
|
||||
nocoal() |
|
||||
for i = 1, 4 do fsend("a") end -- luacheck: no unused |
|
||||
sent() ; fcheck(string.rep("a", 32)) |
|
||||
sent() ; fcheck(string.rep("a", 4)) |
|
||||
sent() ; fchecke() |
|
||||
|
|
||||
-- Hit string length while building up |
|
||||
fsendc("abracadabra overlong") |
|
||||
for i = 1, 10 do fsend(string.rep("a",32)) end -- luacheck: no unused |
|
||||
sent() ; fcheck(string.rep("a", 320)) |
|
||||
sent() ; fchecke() |
|
||||
|
|
||||
-- Hit neither before sending a big string |
|
||||
fsendc("abracadabra mid long") |
|
||||
for i = 1, 6 do fsend(string.rep("a",32)) end -- luacheck: no unused |
|
||||
fsend(string.rep("b", 256)) |
|
||||
nocoal() |
|
||||
for i = 1, 6 do fsend(string.rep("c",32)) end -- luacheck: no unused |
|
||||
sent() ; fcheck(string.rep("a", 192) .. string.rep("b", 256)) |
|
||||
sent() ; fcheck(string.rep("c", 192)) |
|
||||
sent() ; fchecke() |
|
||||
|
|
||||
-- send a huge string, verify that it coalesces |
|
||||
fsendc(string.rep("a",256) .. string.rep("b", 256) .. string.rep("c", 260)) |
|
||||
sent() ; fchecke() |
|
||||
|
|
||||
-- send a huge string, verify that it coalesces save for the short bit at the end |
|
||||
fsend(string.rep("a",256) .. string.rep("b", 256) .. string.rep("c", 256) .. string.rep("d",256)) |
|
||||
fsend("e") |
|
||||
fcheck(string.rep("a",256) .. string.rep("b", 256) .. string.rep("c", 256)) |
|
||||
sent() ; fcheck(string.rep("d",256) .. "e") |
|
||||
sent() ; fchecke() |
|
||||
|
|
||||
-- send enough that our 4x lookahead still leaves something in the queue |
|
||||
fsend(string.rep("a",512) .. string.rep("b", 512) .. string.rep("c", 512)) |
|
||||
fcheck(string.rep("a",512) .. string.rep("b", 512)) |
|
||||
sent() ; fcheck(string.rep("c",512)) |
|
||||
sent() ; fchecke() |
|
||||
|
|
||||
-- test a lazy generator |
|
||||
do |
|
||||
local ix = 0 |
|
||||
local function gen() vprint("GEN", ix); ix = ix + 1; return ("a" .. ix), ix < 3 and gen end |
|
||||
fsend(gen) |
|
||||
fsend("b") |
|
||||
fcheck("a1") |
|
||||
sent() ; fcheck("a2") |
|
||||
sent() ; fcheck("a3") |
|
||||
sent() ; fcheck("b") |
|
||||
sent() ; fchecke() |
|
||||
end |
|
||||
-- test a completion-like callback that does send text |
|
||||
do |
|
||||
local ix = 0 |
|
||||
local function gen() vprint("GEN"); ix = 1; return "efgh", nil end |
|
||||
fsend("abcd"); fsend(gen); fsend("ijkl") |
|
||||
assert (ix == 0) |
|
||||
fcheck("abcd"); assert (ix == 0) |
|
||||
sent() ; fcheck("efgh"); assert (ix == 1); ix = 0 |
|
||||
sent() ; fcheck("ijkl"); assert (ix == 0) |
|
||||
sent() ; fchecke() |
|
||||
end |
|
||||
-- and one that doesn't |
|
||||
do |
|
||||
local ix = 0 |
|
||||
local function gen() vprint("GEN"); ix = 1; return nil, nil end |
|
||||
fsend("abcd"); fsend(gen); fsend("ijkl") |
|
||||
assert (ix == 0) |
|
||||
fcheck("abcd"); assert (ix == 0) |
|
||||
sent() ; fcheck("ijkl"); assert (ix == 1); ix = 0 |
|
||||
sent() ; fchecke() ; assert (ix == 0) |
|
||||
end |
|
||||
print("All tests OK") |
|
@ -1,217 +0,0 @@ |
|||||
------------------------------------------------------------------------------ |
|
||||
-- HTTP server module |
|
||||
-- |
|
||||
-- LICENCE: http://opensource.org/licenses/MIT |
|
||||
-- Vladimir Dronnikov <dronnikov@gmail.com> |
|
||||
------------------------------------------------------------------------------ |
|
||||
local collectgarbage, tonumber, tostring = collectgarbage, tonumber, tostring |
|
||||
|
|
||||
local http |
|
||||
do |
|
||||
------------------------------------------------------------------------------ |
|
||||
-- request methods |
|
||||
------------------------------------------------------------------------------ |
|
||||
local make_req = function(conn, method, url) |
|
||||
return { |
|
||||
conn = conn, |
|
||||
method = method, |
|
||||
url = url, |
|
||||
} |
|
||||
end |
|
||||
|
|
||||
------------------------------------------------------------------------------ |
|
||||
-- response methods |
|
||||
------------------------------------------------------------------------------ |
|
||||
local make_res = function(csend, cfini) |
|
||||
local send = function(self, data, status) |
|
||||
-- TODO: req.send should take care of response headers! |
|
||||
if self.send_header then |
|
||||
csend("HTTP/1.1 ") |
|
||||
csend(tostring(status or 200)) |
|
||||
-- TODO: real HTTP status code/name table |
|
||||
csend(" OK\r\n") |
|
||||
-- we use chunked transfer encoding, to not deal with Content-Length: |
|
||||
-- response header |
|
||||
self:send_header("Transfer-Encoding", "chunked") |
|
||||
-- TODO: send standard response headers, such as Server:, Date: |
|
||||
end |
|
||||
if data then |
|
||||
-- NB: no headers allowed after response body started |
|
||||
if self.send_header then |
|
||||
self.send_header = nil |
|
||||
-- end response headers |
|
||||
csend("\r\n") |
|
||||
end |
|
||||
-- chunked transfer encoding |
|
||||
csend(("%X\r\n"):format(#data)) |
|
||||
csend(data) |
|
||||
csend("\r\n") |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
local send_header = function(_, name, value) |
|
||||
-- NB: quite a naive implementation |
|
||||
csend(name) |
|
||||
csend(": ") |
|
||||
csend(value) |
|
||||
csend("\r\n") |
|
||||
end |
|
||||
-- finalize request, optionally sending data |
|
||||
local finish = function(self, data, status) |
|
||||
-- NB: res.send takes care of response headers |
|
||||
if data then |
|
||||
self:send(data, status) |
|
||||
end |
|
||||
-- finalize chunked transfer encoding |
|
||||
csend("0\r\n\r\n") |
|
||||
-- close connection |
|
||||
cfini() |
|
||||
end |
|
||||
-- |
|
||||
local res = { } |
|
||||
res.send_header = send_header |
|
||||
res.send = send |
|
||||
res.finish = finish |
|
||||
return res |
|
||||
end |
|
||||
|
|
||||
------------------------------------------------------------------------------ |
|
||||
-- HTTP parser |
|
||||
------------------------------------------------------------------------------ |
|
||||
local http_handler = function(handler) |
|
||||
return function(conn) |
|
||||
local csend = (require "fifosock").wrap(conn) |
|
||||
|
|
||||
local req, res |
|
||||
local buf = "" |
|
||||
local method, url |
|
||||
|
|
||||
local ondisconnect = function(connection) |
|
||||
connection:on("receive", nil) |
|
||||
connection:on("disconnection", nil) |
|
||||
connection:on("sent", nil) |
|
||||
collectgarbage("collect") |
|
||||
end |
|
||||
|
|
||||
local cfini = function() |
|
||||
csend(function() |
|
||||
conn:on("sent", nil) |
|
||||
conn:close() |
|
||||
ondisconnect(conn) |
|
||||
end) |
|
||||
end |
|
||||
|
|
||||
|
|
||||
-- header parser |
|
||||
local cnt_len = 0 |
|
||||
|
|
||||
local onheader = function(_, k, v) |
|
||||
-- TODO: look for Content-Type: header |
|
||||
-- to help parse body |
|
||||
-- parse content length to know body length |
|
||||
if k == "content-length" then |
|
||||
cnt_len = tonumber(v) |
|
||||
end |
|
||||
if k == "expect" and v == "100-continue" then |
|
||||
csend("HTTP/1.1 100 Continue\r\n") |
|
||||
end |
|
||||
-- delegate to request object |
|
||||
if req and req.onheader then |
|
||||
req:onheader(k, v) |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
-- body data handler |
|
||||
local body_len = 0 |
|
||||
local ondata = function(_, chunk) |
|
||||
-- feed request data to request handler |
|
||||
if not req or not req.ondata then return end |
|
||||
req:ondata(chunk) |
|
||||
-- NB: once length of seen chunks equals Content-Length: |
|
||||
-- ondata(conn) is called |
|
||||
body_len = body_len + #chunk |
|
||||
-- print("-B", #chunk, body_len, cnt_len, node.heap()) |
|
||||
if body_len >= cnt_len then |
|
||||
req:ondata() |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
local onreceive = function(connection, chunk) |
|
||||
-- merge chunks in buffer |
|
||||
if buf then |
|
||||
buf = buf .. chunk |
|
||||
else |
|
||||
buf = chunk |
|
||||
end |
|
||||
-- consume buffer line by line |
|
||||
while #buf > 0 do |
|
||||
-- extract line |
|
||||
local e = buf:find("\r\n", 1, true) |
|
||||
if not e then break end |
|
||||
local line = buf:sub(1, e - 1) |
|
||||
buf = buf:sub(e + 2) |
|
||||
-- method, url? |
|
||||
if not method then |
|
||||
do |
|
||||
local _ |
|
||||
-- NB: just version 1.1 assumed |
|
||||
_, _, method, url = line:find("^([A-Z]+) (.-) HTTP/1.1$") |
|
||||
end |
|
||||
if method then |
|
||||
-- make request and response objects |
|
||||
req = make_req(connection, method, url) |
|
||||
res = make_res(csend, cfini) |
|
||||
end |
|
||||
-- spawn request handler |
|
||||
handler(req, res) |
|
||||
-- header line? |
|
||||
elseif #line > 0 then |
|
||||
-- parse header |
|
||||
local _, _, k, v = line:find("^([%w-]+):%s*(.+)") |
|
||||
-- header seems ok? |
|
||||
if k then |
|
||||
k = k:lower() |
|
||||
onheader(connection, k, v) |
|
||||
end |
|
||||
-- headers end |
|
||||
else |
|
||||
-- NB: we explicitly reassign receive handler so that |
|
||||
-- next received chunks go directly to body handler |
|
||||
connection:on("receive", ondata) |
|
||||
-- NB: we feed the rest of the buffer as starting chunk of body |
|
||||
ondata(connection, buf) |
|
||||
-- buffer no longer needed |
|
||||
buf = nil |
|
||||
-- parser done |
|
||||
break |
|
||||
end |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
conn:on("receive", onreceive) |
|
||||
conn:on("disconnection", ondisconnect) |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
------------------------------------------------------------------------------ |
|
||||
-- HTTP server |
|
||||
------------------------------------------------------------------------------ |
|
||||
local srv |
|
||||
local createServer = function(port, handler) |
|
||||
-- NB: only one server at a time |
|
||||
if srv then srv:close() end |
|
||||
srv = net.createServer(net.TCP, 15) |
|
||||
-- listen |
|
||||
srv:listen(port, http_handler(handler)) |
|
||||
return srv |
|
||||
end |
|
||||
|
|
||||
------------------------------------------------------------------------------ |
|
||||
-- HTTP server methods |
|
||||
------------------------------------------------------------------------------ |
|
||||
http = { |
|
||||
createServer = createServer, |
|
||||
} |
|
||||
end |
|
||||
|
|
||||
return http |
|
Binary file not shown.
Loading…
Reference in new issue