Merge pull request #5373 from nim-lang/feature/async-streams

Async streams and HTTP client streaming downloads support
This commit is contained in:
Andreas Rumpf 2017-02-26 17:35:08 +01:00 • committed by GitHub
commit e5341a391d
11 changed files with 387 additions and 94 deletions

View file

@ -1387,6 +1387,17 @@ proc send*(socket: AsyncFD, data: string,
# -- Await Macro # -- Await Macro
include asyncmacro include asyncmacro
proc readAll*(future: FutureStream[string]): Future[string] {.async.} =
## Returns a future that will complete when all the string data from the
## specified future stream is retrieved.
result = ""
while true:
let (hasValue, value) = await future.read()
if hasValue:
result.add(value)
else:
break
proc recvLine*(socket: AsyncFD): Future[string] {.async, deprecated.} = proc recvLine*(socket: AsyncFD): Future[string] {.async, deprecated.} =
## Reads a line of data from ``socket``. Returned future will complete once ## Reads a line of data from ``socket``. Returned future will complete once
## a full line is read or an error occurs. ## a full line is read or an error occurs.

View file

@ -476,3 +476,16 @@ proc close*(f: AsyncFile) =
if close(f.fd.cint) == -1: if close(f.fd.cint) == -1:
raiseOSError(osLastError()) raiseOSError(osLastError())
proc writeFromStream*(f: AsyncFile, fs: FutureStream[string]) {.async.} =
## Reads data from the specified future stream until it is completed.
## The data which is read is written to the file immediately and
## freed from memory.
##
## This procedure is perfect for saving streamed data to a file without
## wasting memory.
while true:
let (hasValue, value) = await fs.read()
if hasValue:
await f.write(value)
else:
break

View file

@ -33,8 +33,10 @@ template createCb(retFutureSym, iteratorNameSym,
if not nameIterVar.finished: if not nameIterVar.finished:
var next = nameIterVar() var next = nameIterVar()
if next == nil: if next == nil:
assert retFutureSym.finished, "Async procedure's (" & if not retFutureSym.finished:
name & ") return Future was not finished." let msg = "Async procedure ($1) yielded `nil`, are you await'ing a " &
"`nil` Future?"
raise newException(AssertionError, msg % name)
else: else:
next.callback = cb next.callback = cb
except: except:
@ -281,6 +283,14 @@ proc getFutureVarIdents(params: NimNode): seq[NimNode] {.compileTime.} =
($params[i][1][0].ident).normalize == "futurevar": ($params[i][1][0].ident).normalize == "futurevar":
result.add(params[i][0]) result.add(params[i][0])
proc isInvalidReturnType(typeName: string): bool =
return typeName notin ["Future"] #, "FutureStream"]
proc verifyReturnType(typeName: string) {.compileTime.} =
if typeName.isInvalidReturnType:
error("Expected return type of 'Future' got '$1'" %
typeName)
proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
## This macro transforms a single procedure into a closure iterator. ## This macro transforms a single procedure into a closure iterator.
## The ``async`` macro supports a stmtList holding multiple async procedures. ## The ``async`` macro supports a stmtList holding multiple async procedures.
@ -295,18 +305,16 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
# Verify that the return type is a Future[T] # Verify that the return type is a Future[T]
if returnType.kind == nnkBracketExpr: if returnType.kind == nnkBracketExpr:
let fut = repr(returnType[0]) let fut = repr(returnType[0])
if fut != "Future": verifyReturnType(fut)
error("Expected return type of 'Future' got '" & fut & "'")
baseType = returnType[1] baseType = returnType[1]
elif returnType.kind in nnkCallKinds and $returnType[0] == "[]": elif returnType.kind in nnkCallKinds and $returnType[0] == "[]":
let fut = repr(returnType[1]) let fut = repr(returnType[1])
if fut != "Future": verifyReturnType(fut)
error("Expected return type of 'Future' got '" & fut & "'")
baseType = returnType[2] baseType = returnType[2]
elif returnType.kind == nnkEmpty: elif returnType.kind == nnkEmpty:
baseType = returnType baseType = returnType
else: else:
error("Expected return type of 'Future' got '" & repr(returnType) & "'") verifyReturnType(repr(returnType))
let subtypeIsVoid = returnType.kind == nnkEmpty or let subtypeIsVoid = returnType.kind == nnkEmpty or
(baseType.kind == nnkIdent and returnType[1].ident == !"void") (baseType.kind == nnkIdent and returnType[1].ident == !"void")
@ -390,7 +398,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
if procBody.kind != nnkEmpty: if procBody.kind != nnkEmpty:
result[6] = outerProcBody result[6] = outerProcBody
#echo(treeRepr(result)) #echo(treeRepr(result))
#if prc[0].getName == "testInfix": #if prc[0].getName == "beta":
# echo(toStrLit(result)) # echo(toStrLit(result))
macro async*(prc: untyped): untyped = macro async*(prc: untyped): untyped =
@ -451,13 +459,12 @@ proc stripAwait(node: NimNode): NimNode =
for i in 0 .. <result.len: for i in 0 .. <result.len:
result[i] = stripAwait(result[i]) result[i] = stripAwait(result[i])
proc splitParams(param: NimNode, async: bool): NimNode = proc splitParamType(paramType: NimNode, async: bool): NimNode =
expectKind(param, nnkIdentDefs) result = paramType
result = param if paramType.kind == nnkInfix and $paramType[0].ident in ["|", "or"]:
if param[1].kind == nnkInfix and $param[1][0].ident in ["|", "or"]: let firstType = paramType[1]
let firstType = param[1][1]
let firstTypeName = $firstType.ident let firstTypeName = $firstType.ident
let secondType = param[1][2] let secondType = paramType[2]
let secondTypeName = $secondType.ident let secondTypeName = $secondType.ident
# Make sure that at least one has the name `async`, otherwise we shouldn't # Make sure that at least one has the name `async`, otherwise we shouldn't
@ -468,22 +475,21 @@ proc splitParams(param: NimNode, async: bool): NimNode =
if async: if async:
if firstTypeName.normalize.startsWith("async"): if firstTypeName.normalize.startsWith("async"):
result = newIdentDefs(param[0], param[1][1]) result = paramType[1]
elif secondTypeName.normalize.startsWith("async"): elif secondTypeName.normalize.startsWith("async"):
result = newIdentDefs(param[0], param[1][2]) result = paramType[2]
else: else:
if not firstTypeName.normalize.startsWith("async"): if not firstTypeName.normalize.startsWith("async"):
result = newIdentDefs(param[0], param[1][1]) result = paramType[1]
elif not secondTypeName.normalize.startsWith("async"): elif not secondTypeName.normalize.startsWith("async"):
result = newIdentDefs(param[0], param[1][2]) result = paramType[2]
proc stripReturnType(returnType: NimNode): NimNode = proc stripReturnType(returnType: NimNode): NimNode =
# Strip out the 'Future' from 'Future[T]'. # Strip out the 'Future' from 'Future[T]'.
result = returnType result = returnType
if returnType.kind == nnkBracketExpr: if returnType.kind == nnkBracketExpr:
let fut = repr(returnType[0]) let fut = repr(returnType[0])
if fut != "Future": verifyReturnType(fut)
error("Expected return type of 'Future' got '" & fut & "'")
result = returnType[1] result = returnType[1]
proc splitProc(prc: NimNode): (NimNode, NimNode) = proc splitProc(prc: NimNode): (NimNode, NimNode) =
@ -491,15 +497,24 @@ proc splitProc(prc: NimNode): (NimNode, NimNode) =
## for example: proc (socket: Socket | AsyncSocket). ## for example: proc (socket: Socket | AsyncSocket).
## It transforms them so that ``proc (socket: Socket)`` and ## It transforms them so that ``proc (socket: Socket)`` and
## ``proc (socket: AsyncSocket)`` are returned. ## ``proc (socket: AsyncSocket)`` are returned.
result[0] = prc.copyNimTree() result[0] = prc.copyNimTree()
result[0][3][0] = stripReturnType(result[0][3][0]) # Retrieve the `T` inside `Future[T]`.
let returnType = stripReturnType(result[0][3][0])
result[0][3][0] = splitParamType(returnType, async=false)
for i in 1 .. <result[0][3].len: for i in 1 .. <result[0][3].len:
result[0][3][i] = splitParams(result[0][3][i], false) # Sync proc (0) -> FormalParams (3) -> IdentDefs, the parameter (i) ->
# parameter type (1).
result[0][3][i][1] = splitParamType(result[0][3][i][1], async=false)
result[0][6] = stripAwait(result[0][6]) result[0][6] = stripAwait(result[0][6])
result[1] = prc.copyNimTree() result[1] = prc.copyNimTree()
if result[1][3][0].kind == nnkBracketExpr:
result[1][3][0][1] = splitParamType(result[1][3][0][1], async=true)
for i in 1 .. <result[1][3].len: for i in 1 .. <result[1][3].len:
result[1][3][i] = splitParams(result[1][3][i], true) # Async proc (1) -> FormalParams (3) -> IdentDefs, the parameter (i) ->
# parameter type (1).
result[1][3][i][1] = splitParamType(result[1][3][i][1], async=true)
macro multisync*(prc: untyped): untyped = macro multisync*(prc: untyped): untyped =
## Macro which processes async procedures into both asynchronous and ## Macro which processes async procedures into both asynchronous and
@ -512,4 +527,4 @@ macro multisync*(prc: untyped): untyped =
let (sync, asyncPrc) = splitProc(prc) let (sync, asyncPrc) = splitProc(prc)
result = newStmtList() result = newStmtList()
result.add(asyncSingleProc(asyncPrc)) result.add(asyncSingleProc(asyncPrc))
result.add(sync) result.add(sync)

View file

@ -129,7 +129,7 @@ proc expandIfNeeded[T](deq: var Deque[T]) =
var cap = deq.mask + 1 var cap = deq.mask + 1
if unlikely(deq.count >= cap): if unlikely(deq.count >= cap):
var n = newSeq[T](cap * 2) var n = newSeq[T](cap * 2)
for i, x in deq: # don't use copyMem because the GC and because it's slower. for i, x in pairs(deq): # don't use copyMem because the GC and because it's slower.
shallowCopy(n[i], x) shallowCopy(n[i], x)
shallowCopy(deq.data, n) shallowCopy(deq.data, n)
deq.mask = cap * 2 - 1 deq.mask = cap * 2 - 1

View file

@ -144,7 +144,7 @@ proc add*[T](q: var Queue[T], item: T) =
var cap = q.mask+1 var cap = q.mask+1
if unlikely(q.count >= cap): if unlikely(q.count >= cap):
var n = newSeq[T](cap*2) var n = newSeq[T](cap*2)
for i, x in q: # don't use copyMem because the GC and because it's slower. for i, x in pairs(q): # don't use copyMem because the GC and because it's slower.
shallowCopy(n[i], x) shallowCopy(n[i], x)
shallowCopy(q.data, n) shallowCopy(q.data, n)
q.mask = cap*2 - 1 q.mask = cap*2 - 1

View file

@ -84,6 +84,9 @@
## .. code-block:: Nim ## .. code-block:: Nim
## client.onProgressChanged = nil ## client.onProgressChanged = nil
## ##
## **Warning:** The ``total`` reported by httpclient may be 0 in some cases.
##
##
## SSL/TLS support ## SSL/TLS support
## =============== ## ===============
## This requires the OpenSSL library, fortunately it's widely used and installed ## This requires the OpenSSL library, fortunately it's widely used and installed
@ -117,20 +120,28 @@
## only basic authentication is supported at the moment. ## only basic authentication is supported at the moment.
import net, strutils, uri, parseutils, strtabs, base64, os, mimetypes, import net, strutils, uri, parseutils, strtabs, base64, os, mimetypes,
math, random, httpcore, times, tables math, random, httpcore, times, tables, streams
import asyncnet, asyncdispatch import asyncnet, asyncdispatch, asyncfile
import nativesockets import nativesockets
export httpcore except parseHeader # TODO: The ``except`` doesn't work export httpcore except parseHeader # TODO: The ``except`` doesn't work
type type
Response* = object Response* = ref object
version*: string version*: string
status*: string status*: string
headers*: HttpHeaders headers*: HttpHeaders
body*: string body: string
bodyStream*: Stream
proc code*(response: Response): HttpCode AsyncResponse* = ref object
version*: string
status*: string
headers*: HttpHeaders
body: string
bodyStream*: FutureStream[string]
proc code*(response: Response | AsyncResponse): HttpCode
{.raises: [ValueError, OverflowError].} = {.raises: [ValueError, OverflowError].} =
## Retrieves the specified response's ``HttpCode``. ## Retrieves the specified response's ``HttpCode``.
## ##
@ -138,6 +149,27 @@ proc code*(response: Response): HttpCode
## corresponding ``HttpCode``. ## corresponding ``HttpCode``.
return response.status[0 .. 2].parseInt.HttpCode return response.status[0 .. 2].parseInt.HttpCode
proc body*(response: Response): string =
## Retrieves the specified response's body.
##
## The response's body stream is read synchronously.
if response.body.isNil():
response.body = response.bodyStream.readAll()
return response.body
proc `body=`*(response: Response, value: string) {.deprecated.} =
## Setter for backward compatibility.
##
## **This is deprecated and should not be used**.
response.body = value
proc body*(response: AsyncResponse): Future[string] {.async.} =
## Reads the response's body and caches it. The read is performed only
## once.
if response.body.isNil:
response.body = await readAll(response.bodyStream)
return response.body
type type
Proxy* = ref object Proxy* = ref object
url*: Uri url*: Uri
@ -249,6 +281,7 @@ proc parseBody(s: Socket, headers: HttpHeaders, httpVersion: string, timeout: in
result.add(buf) result.add(buf)
proc parseResponse(s: Socket, getBody: bool, timeout: int): Response = proc parseResponse(s: Socket, getBody: bool, timeout: int): Response =
new result
var parsedStatus = false var parsedStatus = false
var linei = 0 var linei = 0
var fullyRead = false var fullyRead = false
@ -604,7 +637,7 @@ proc post*(url: string, extraHeaders = "", body = "",
## **Deprecated since version 0.15.0**: use ``HttpClient.post`` instead. ## **Deprecated since version 0.15.0**: use ``HttpClient.post`` instead.
let (mpHeaders, mpBody) = format(multipart) let (mpHeaders, mpBody) = format(multipart)
template withNewLine(x): expr = template withNewLine(x): untyped =
if x.len > 0 and not x.endsWith("\c\L"): if x.len > 0 and not x.endsWith("\c\L"):
x & "\c\L" x & "\c\L"
else: else:
@ -653,10 +686,13 @@ proc postContent*(url: string, extraHeaders = "", body = "",
proc downloadFile*(url: string, outputFilename: string, proc downloadFile*(url: string, outputFilename: string,
sslContext: SSLContext = defaultSSLContext, sslContext: SSLContext = defaultSSLContext,
timeout = -1, userAgent = defUserAgent, timeout = -1, userAgent = defUserAgent,
proxy: Proxy = nil) = proxy: Proxy = nil) {.deprecated.} =
## | Downloads ``url`` and saves it to ``outputFilename`` ## | Downloads ``url`` and saves it to ``outputFilename``
## | An optional timeout can be specified in milliseconds, if reading from the ## | An optional timeout can be specified in milliseconds, if reading from the
## server takes longer than specified an ETimeout exception will be raised. ## server takes longer than specified an ETimeout exception will be raised.
##
## **Deprecated since version 0.16.2**: use ``HttpClient.downloadFile``
## instead.
var f: File var f: File
if open(f, outputFilename, fmWrite): if open(f, outputFilename, fmWrite):
f.write(getContent(url, sslContext = sslContext, timeout = timeout, f.write(getContent(url, sslContext = sslContext, timeout = timeout,
@ -735,6 +771,11 @@ type
contentProgress: BiggestInt contentProgress: BiggestInt
oneSecondProgress: BiggestInt oneSecondProgress: BiggestInt
lastProgressReport: float lastProgressReport: float
when SocketType is AsyncSocket:
bodyStream: FutureStream[string]
else:
bodyStream: Stream
getBody: bool ## When `false`, the body is never read in requestAux.
type type
HttpClient* = HttpClientBase[Socket] HttpClient* = HttpClientBase[Socket]
@ -764,6 +805,8 @@ proc newHttpClient*(userAgent = defUserAgent,
result.proxy = proxy result.proxy = proxy
result.timeout = timeout result.timeout = timeout
result.onProgressChanged = nil result.onProgressChanged = nil
result.bodyStream = newStringStream()
result.getBody = true
when defined(ssl): when defined(ssl):
result.sslContext = sslContext result.sslContext = sslContext
@ -794,6 +837,8 @@ proc newAsyncHttpClient*(userAgent = defUserAgent,
result.proxy = proxy result.proxy = proxy
result.timeout = -1 # TODO result.timeout = -1 # TODO
result.onProgressChanged = nil result.onProgressChanged = nil
result.bodyStream = newFutureStream[string]("newAsyncHttpClient")
result.getBody = true
when defined(ssl): when defined(ssl):
result.sslContext = sslContext result.sslContext = sslContext
@ -815,14 +860,14 @@ proc reportProgress(client: HttpClient | AsyncHttpClient,
client.oneSecondProgress = 0 client.oneSecondProgress = 0
client.lastProgressReport = epochTime() client.lastProgressReport = epochTime()
proc recvFull(client: HttpClient | AsyncHttpClient, proc recvFull(client: HttpClient | AsyncHttpClient, size: int, timeout: int,
size: int, timeout: int): Future[string] {.multisync.} = keep: bool): Future[int] {.multisync.} =
## Ensures that all the data requested is read and returned. ## Ensures that all the data requested is read and returned.
result = "" var readLen = 0
while true: while true:
if size == result.len: break if size == readLen: break
let remainingSize = size - result.len let remainingSize = size - readLen
let sizeToRecv = min(remainingSize, net.BufferSize) let sizeToRecv = min(remainingSize, net.BufferSize)
when client.socket is Socket: when client.socket is Socket:
@ -830,13 +875,17 @@ proc recvFull(client: HttpClient | AsyncHttpClient,
else: else:
let data = await client.socket.recv(sizeToRecv) let data = await client.socket.recv(sizeToRecv)
if data == "": break # We've been disconnected. if data == "": break # We've been disconnected.
result.add data
readLen.inc(data.len)
if keep:
await client.bodyStream.write(data)
await reportProgress(client, data.len) await reportProgress(client, data.len)
proc parseChunks(client: HttpClient | AsyncHttpClient): Future[string] return readLen
proc parseChunks(client: HttpClient | AsyncHttpClient): Future[void]
{.multisync.} = {.multisync.} =
result = ""
while true: while true:
var chunkSize = 0 var chunkSize = 0
var chunkSizeStr = await client.socket.recvLine() var chunkSizeStr = await client.socket.recvLine()
@ -861,25 +910,27 @@ proc parseChunks(client: HttpClient | AsyncHttpClient): Future[string]
httpError("Invalid chunk size: " & chunkSizeStr) httpError("Invalid chunk size: " & chunkSizeStr)
inc(i) inc(i)
if chunkSize <= 0: if chunkSize <= 0:
discard await recvFull(client, 2, client.timeout) # Skip \c\L discard await recvFull(client, 2, client.timeout, false) # Skip \c\L
break break
result.add await recvFull(client, chunkSize, client.timeout) discard await recvFull(client, chunkSize, client.timeout, true)
discard await recvFull(client, 2, client.timeout) # Skip \c\L discard await recvFull(client, 2, client.timeout, false) # Skip \c\L
# Trailer headers will only be sent if the request specifies that we want # Trailer headers will only be sent if the request specifies that we want
# them: http://tools.ietf.org/html/rfc2616#section-3.6.1 # them: http://tools.ietf.org/html/rfc2616#section-3.6.1
proc parseBody(client: HttpClient | AsyncHttpClient, proc parseBody(client: HttpClient | AsyncHttpClient,
headers: HttpHeaders, headers: HttpHeaders,
httpVersion: string): Future[string] {.multisync.} = httpVersion: string): Future[void] {.multisync.} =
result = ""
# Reset progress from previous requests. # Reset progress from previous requests.
client.contentTotal = 0 client.contentTotal = 0
client.contentProgress = 0 client.contentProgress = 0
client.oneSecondProgress = 0 client.oneSecondProgress = 0
client.lastProgressReport = 0 client.lastProgressReport = 0
when client is AsyncHttpClient:
assert(not client.bodyStream.finished)
if headers.getOrDefault"Transfer-Encoding" == "chunked": if headers.getOrDefault"Transfer-Encoding" == "chunked":
result = await parseChunks(client) await parseChunks(client)
else: else:
# -REGION- Content-Length # -REGION- Content-Length
# (http://tools.ietf.org/html/rfc2616#section-4.4) NR.3 # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.3
@ -888,26 +939,31 @@ proc parseBody(client: HttpClient | AsyncHttpClient,
var length = contentLengthHeader.parseint() var length = contentLengthHeader.parseint()
client.contentTotal = length client.contentTotal = length
if length > 0: if length > 0:
result = await client.recvFull(length, client.timeout) let recvLen = await client.recvFull(length, client.timeout, true)
if result == "": if recvLen == 0:
httpError("Got disconnected while trying to read body.") httpError("Got disconnected while trying to read body.")
if result.len != length: if recvLen != length:
httpError("Received length doesn't match expected length. Wanted " & httpError("Received length doesn't match expected length. Wanted " &
$length & " got " & $result.len) $length & " got " & $recvLen)
else: else:
# (http://tools.ietf.org/html/rfc2616#section-4.4) NR.4 TODO # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.4 TODO
# -REGION- Connection: Close # -REGION- Connection: Close
# (http://tools.ietf.org/html/rfc2616#section-4.4) NR.5 # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.5
if headers.getOrDefault"Connection" == "close" or httpVersion == "1.0": if headers.getOrDefault"Connection" == "close" or httpVersion == "1.0":
var buf = ""
while true: while true:
buf = await client.recvFull(4000, client.timeout) let recvLen = await client.recvFull(4000, client.timeout, true)
if buf == "": break if recvLen == 0: break
result.add(buf)
when client is AsyncHttpClient:
client.bodyStream.complete()
else:
client.bodyStream.setPosition(0)
proc parseResponse(client: HttpClient | AsyncHttpClient, proc parseResponse(client: HttpClient | AsyncHttpClient,
getBody: bool): Future[Response] {.multisync.} = getBody: bool): Future[Response | AsyncResponse]
{.multisync.} =
new result
var parsedStatus = false var parsedStatus = false
var linei = 0 var linei = 0
var fullyRead = false var fullyRead = false
@ -955,10 +1011,14 @@ proc parseResponse(client: HttpClient | AsyncHttpClient,
if not fullyRead: if not fullyRead:
httpError("Connection was closed before full request has been made") httpError("Connection was closed before full request has been made")
if getBody: if getBody:
result.body = await parseBody(client, result.headers, result.version) when client is HttpClient:
else: client.bodyStream = newStringStream()
result.body = "" else:
client.bodyStream = newFutureStream[string]("parseResponse")
await parseBody(client, result.headers, result.version)
result.bodyStream = client.bodyStream
proc newConnection(client: HttpClient | AsyncHttpClient, proc newConnection(client: HttpClient | AsyncHttpClient,
url: Uri) {.multisync.} = url: Uri) {.multisync.} =
@ -1006,8 +1066,9 @@ proc override(fallback, override: HttpHeaders): HttpHeaders =
result[k] = vs result[k] = vs
proc requestAux(client: HttpClient | AsyncHttpClient, url: string, proc requestAux(client: HttpClient | AsyncHttpClient, url: string,
httpMethod: string, body = "", httpMethod: string, body = "",
headers: HttpHeaders = nil): Future[Response] {.multisync.} = headers: HttpHeaders = nil): Future[Response | AsyncResponse]
{.multisync.} =
# Helper that actually makes the request. Does not handle redirects. # Helper that actually makes the request. Does not handle redirects.
let connectionUrl = let connectionUrl =
if client.proxy.isNil: parseUri(url) else: client.proxy.url if client.proxy.isNil: parseUri(url) else: client.proxy.url
@ -1047,16 +1108,17 @@ proc requestAux(client: HttpClient | AsyncHttpClient, url: string,
if body != "": if body != "":
await client.socket.send(body) await client.socket.send(body)
result = await parseResponse(client, let getBody = httpMethod.toLowerAscii() notin ["head", "connect"] and
httpMethod.toLower() notin ["head", "connect"]) client.getBody
result = await parseResponse(client, getBody)
# Restore the clients proxy in case it was overwritten. # Restore the clients proxy in case it was overwritten.
client.proxy = savedProxy client.proxy = savedProxy
proc request*(client: HttpClient | AsyncHttpClient, url: string, proc request*(client: HttpClient | AsyncHttpClient, url: string,
httpMethod: string, body = "", httpMethod: string, body = "",
headers: HttpHeaders = nil): Future[Response] {.multisync.} = headers: HttpHeaders = nil): Future[Response | AsyncResponse]
{.multisync.} =
## Connects to the hostname specified by the URL and performs a request ## Connects to the hostname specified by the URL and performs a request
## using the custom method string specified by ``httpMethod``. ## using the custom method string specified by ``httpMethod``.
## ##
@ -1078,7 +1140,8 @@ proc request*(client: HttpClient | AsyncHttpClient, url: string,
proc request*(client: HttpClient | AsyncHttpClient, url: string, proc request*(client: HttpClient | AsyncHttpClient, url: string,
httpMethod = HttpGET, body = "", httpMethod = HttpGET, body = "",
headers: HttpHeaders = nil): Future[Response] {.multisync.} = headers: HttpHeaders = nil): Future[Response | AsyncResponse]
{.multisync.} =
## Connects to the hostname specified by the URL and performs a request ## Connects to the hostname specified by the URL and performs a request
## using the method specified. ## using the method specified.
## ##
@ -1088,11 +1151,10 @@ proc request*(client: HttpClient | AsyncHttpClient, url: string,
## ##
## When a request is made to a different hostname, the current connection will ## When a request is made to a different hostname, the current connection will
## be closed. ## be closed.
result = await request(client, url, $httpMethod, body, result = await request(client, url, $httpMethod, body, headers)
headers = headers)
proc get*(client: HttpClient | AsyncHttpClient, proc get*(client: HttpClient | AsyncHttpClient,
url: string): Future[Response] {.multisync.} = url: string): Future[Response | AsyncResponse] {.multisync.} =
## Connects to the hostname specified by the URL and performs a GET request. ## Connects to the hostname specified by the URL and performs a GET request.
## ##
## This procedure will follow redirects up to a maximum number of redirects ## This procedure will follow redirects up to a maximum number of redirects
@ -1112,17 +1174,18 @@ proc getContent*(client: HttpClient | AsyncHttpClient,
if resp.code.is4xx or resp.code.is5xx: if resp.code.is4xx or resp.code.is5xx:
raise newException(HttpRequestError, resp.status) raise newException(HttpRequestError, resp.status)
else: else:
return resp.body return await resp.bodyStream.readAll()
proc post*(client: HttpClient | AsyncHttpClient, url: string, body = "", proc post*(client: HttpClient | AsyncHttpClient, url: string, body = "",
multipart: MultipartData = nil): Future[Response] {.multisync.} = multipart: MultipartData = nil): Future[Response | AsyncResponse]
{.multisync.} =
## Connects to the hostname specified by the URL and performs a POST request. ## Connects to the hostname specified by the URL and performs a POST request.
## ##
## This procedure will follow redirects up to a maximum number of redirects ## This procedure will follow redirects up to a maximum number of redirects
## specified in ``client.maxRedirects``. ## specified in ``client.maxRedirects``.
let (mpHeader, mpBody) = format(multipart) let (mpHeader, mpBody) = format(multipart)
# TODO: Support FutureStream for `body` parameter.
template withNewLine(x): expr = template withNewLine(x): untyped =
if x.len > 0 and not x.endsWith("\c\L"): if x.len > 0 and not x.endsWith("\c\L"):
x & "\c\L" x & "\c\L"
else: else:
@ -1134,16 +1197,14 @@ proc post*(client: HttpClient | AsyncHttpClient, url: string, body = "",
headers["Content-Type"] = mpHeader.split(": ")[1] headers["Content-Type"] = mpHeader.split(": ")[1]
headers["Content-Length"] = $len(xb) headers["Content-Length"] = $len(xb)
result = await client.requestAux(url, $HttpPOST, xb, result = await client.requestAux(url, $HttpPOST, xb, headers)
headers = headers)
# Handle redirects. # Handle redirects.
var lastURL = url var lastURL = url
for i in 1..client.maxRedirects: for i in 1..client.maxRedirects:
if result.status.redirection(): if result.status.redirection():
let redirectTo = getNewLocation(lastURL, result.headers) let redirectTo = getNewLocation(lastURL, result.headers)
var meth = if result.status != "307": HttpGet else: HttpPost var meth = if result.status != "307": HttpGet else: HttpPost
result = await client.requestAux(redirectTo, $meth, xb, result = await client.requestAux(redirectTo, $meth, xb, headers)
headers = headers)
lastURL = redirectTo lastURL = redirectTo
proc postContent*(client: HttpClient | AsyncHttpClient, url: string, proc postContent*(client: HttpClient | AsyncHttpClient, url: string,
@ -1161,4 +1222,30 @@ proc postContent*(client: HttpClient | AsyncHttpClient, url: string,
if resp.code.is4xx or resp.code.is5xx: if resp.code.is4xx or resp.code.is5xx:
raise newException(HttpRequestError, resp.status) raise newException(HttpRequestError, resp.status)
else: else:
return resp.body return await resp.bodyStream.readAll()
proc downloadFile*(client: HttpClient | AsyncHttpClient,
url: string, filename: string): Future[void] {.multisync.} =
## Downloads ``url`` and saves it to ``filename``.
client.getBody = false
let resp = await client.get(url)
when client is HttpClient:
client.bodyStream = newFileStream(filename, fmWrite)
if client.bodyStream.isNil:
fileError("Unable to open file")
parseBody(client, resp.headers, resp.version)
client.bodyStream.close()
else:
client.bodyStream = newFutureStream[string]("downloadFile")
var file = openAsync(filename, fmWrite)
# Let `parseBody` write response data into client.bodyStream in the
# background.
asyncCheck parseBody(client, resp.headers, resp.version)
# The `writeFromStream` proc will complete once all the data in the
# `bodyStream` has been written to the file.
await file.writeFromStream(client.bodyStream)
file.close()
if resp.code.is4xx or resp.code.is5xx:
raise newException(HttpRequestError, resp.status)

View file

@ -16,6 +16,12 @@ type
FutureVar*[T] = distinct Future[T] FutureVar*[T] = distinct Future[T]
FutureStream*[T] = ref object of FutureBase ## Special future that acts as
## a queue. Its API is still
## experimental and so is
## subject to change.
queue: Deque[T]
FutureError* = object of Exception FutureError* = object of Exception
cause*: FutureBase cause*: FutureBase
@ -26,11 +32,7 @@ when not defined(release):
proc callSoon*(cbproc: proc ()) {.gcsafe.} proc callSoon*(cbproc: proc ()) {.gcsafe.}
proc newFuture*[T](fromProc: string = "unspecified"): Future[T] = template setupFutureBase(fromProc: string) =
## Creates a new future.
##
## Specifying ``fromProc``, which is a string specifying the name of the proc
## that this future belongs to, is a good habit as it helps with debugging.
new(result) new(result)
result.finished = false result.finished = false
when not defined(release): when not defined(release):
@ -39,6 +41,13 @@ proc newFuture*[T](fromProc: string = "unspecified"): Future[T] =
result.fromProc = fromProc result.fromProc = fromProc
currentID.inc() currentID.inc()
proc newFuture*[T](fromProc: string = "unspecified"): Future[T] =
## Creates a new future.
##
## Specifying ``fromProc``, which is a string specifying the name of the proc
## that this future belongs to, is a good habit as it helps with debugging.
setupFutureBase(fromProc)
proc newFutureVar*[T](fromProc = "unspecified"): FutureVar[T] = proc newFutureVar*[T](fromProc = "unspecified"): FutureVar[T] =
## Create a new ``FutureVar``. This Future type is ideally suited for ## Create a new ``FutureVar``. This Future type is ideally suited for
## situations where you want to avoid unnecessary allocations of Futures. ## situations where you want to avoid unnecessary allocations of Futures.
@ -47,6 +56,22 @@ proc newFutureVar*[T](fromProc = "unspecified"): FutureVar[T] =
## that this future belongs to, is a good habit as it helps with debugging. ## that this future belongs to, is a good habit as it helps with debugging.
result = FutureVar[T](newFuture[T](fromProc)) result = FutureVar[T](newFuture[T](fromProc))
proc newFutureStream*[T](fromProc = "unspecified"): FutureStream[T] =
## Create a new ``FutureStream``. This future's callback is activated when
## two events occur:
##
## * New data is written into the future stream.
## * The future stream is completed (this means that no more data will be
## written).
##
## Specifying ``fromProc``, which is a string specifying the name of the proc
## that this future belongs to, is a good habit as it helps with debugging.
##
## **Note:** The API of FutureStream is still new and so has a higher
## likelihood of changing in the future.
setupFutureBase(fromProc)
result.queue = initDeque[T]()
proc clean*[T](future: FutureVar[T]) = proc clean*[T](future: FutureVar[T]) =
## Resets the ``finished`` status of ``future``. ## Resets the ``finished`` status of ``future``.
Future[T](future).finished = false Future[T](future).finished = false
@ -107,12 +132,18 @@ proc complete*[T](future: FutureVar[T], val: T) =
## Any previously stored value will be overwritten. ## Any previously stored value will be overwritten.
template fut: untyped = Future[T](future) template fut: untyped = Future[T](future)
checkFinished(fut) checkFinished(fut)
assert(fut.error == nil) assert(fut.error.isNil())
fut.finished = true fut.finished = true
fut.value = val fut.value = val
if fut.cb != nil: if not fut.cb.isNil():
fut.cb() fut.cb()
proc complete*[T](future: FutureStream[T]) =
## Completes a ``FutureStream`` signalling the end of data.
future.finished = true
if not future.cb.isNil():
future.cb()
proc fail*[T](future: Future[T], error: ref Exception) = proc fail*[T](future: Future[T], error: ref Exception) =
## Completes ``future`` with ``error``. ## Completes ``future`` with ``error``.
#assert(not future.finished, "Future already finished, cannot finish twice.") #assert(not future.finished, "Future already finished, cannot finish twice.")
@ -149,6 +180,20 @@ proc `callback=`*[T](future: Future[T],
## If future has already completed then ``cb`` will be called immediately. ## If future has already completed then ``cb`` will be called immediately.
future.callback = proc () = cb(future) future.callback = proc () = cb(future)
proc `callback=`*[T](future: FutureStream[T],
cb: proc (future: FutureStream[T]) {.closure,gcsafe.}) =
## Sets the callback proc to be called when data was placed inside the
## future stream.
##
## The callback is also called when the future is completed. So you should
## use ``finished`` to check whether data is available.
##
## If the future stream already has data or is finished then ``cb`` will be
## called immediately.
future.cb = proc () = cb(future)
if future.queue.len > 0 or future.finished:
callSoon(future.cb)
proc injectStacktrace[T](future: Future[T]) = proc injectStacktrace[T](future: Future[T]) =
# TODO: Come up with something better. # TODO: Come up with something better.
when not defined(release): when not defined(release):
@ -195,12 +240,18 @@ proc mget*[T](future: FutureVar[T]): var T =
## Future has not been finished. ## Future has not been finished.
result = Future[T](future).value result = Future[T](future).value
proc finished*[T](future: Future[T] | FutureVar[T]): bool = proc finished*[T](future: Future[T] | FutureVar[T] | FutureStream[T]): bool =
## Determines whether ``future`` has completed. ## Determines whether ``future`` has completed.
## ##
## ``True`` may indicate an error or a value. Use ``failed`` to distinguish. ## ``True`` may indicate an error or a value. Use ``failed`` to distinguish.
##
## For a ``FutureStream`` a ``true`` value means that no more data will be
## placed inside the stream _and_ that there is no data waiting to be
## retrieved.
when future is FutureVar[T]: when future is FutureVar[T]:
result = (Future[T](future)).finished result = (Future[T](future)).finished
elif future is FutureStream[T]:
result = future.finished and future.queue.len == 0
else: else:
result = future.finished result = future.finished
@ -208,6 +259,57 @@ proc failed*(future: FutureBase): bool =
## Determines whether ``future`` completed with an error. ## Determines whether ``future`` completed with an error.
return future.error != nil return future.error != nil
proc write*[T](future: FutureStream[T], value: T): Future[void] =
## Writes the specified value inside the specified future stream.
##
## This will raise ``ValueError`` if ``future`` is finished.
result = newFuture[void]("FutureStream.put")
if future.finished:
let msg = "FutureStream is finished and so no longer accepts new data."
result.fail(newException(ValueError, msg))
return
# TODO: Implement limiting of the streams storage to prevent it growing
# infinitely when no reads are occuring.
future.queue.addLast(value)
if not future.cb.isNil: future.cb()
result.complete()
proc read*[T](future: FutureStream[T]): Future[(bool, T)] =
## Returns a future that will complete when the ``FutureStream`` has data
## placed into it. The future will be completed with the oldest
## value stored inside the stream. The return value will also determine
## whether data was retrieved, ``false`` means that the future stream was
## completed and no data was retrieved.
##
## This function will remove the data that was returned from the underlying
## ``FutureStream``.
var resFut = newFuture[(bool, T)]("FutureStream.take")
let savedCb = future.cb
future.callback =
proc (fs: FutureStream[T]) =
# We don't want this callback called again.
future.cb = nil
# The return value depends on whether the FutureStream has finished.
var res: (bool, T)
if finished(fs):
# Remember, this callback is called when the FutureStream is completed.
res[0] = false
else:
res[0] = true
res[1] = fs.queue.popFirst()
if not resFut.finished:
resFut.complete(res)
# If the saved callback isn't nil then let's call it.
if not savedCb.isNil: savedCb()
return resFut
proc len*[T](future: FutureStream[T]): int =
## Returns the amount of data pieces inside the stream.
future.queue.len
proc asyncCheck*[T](future: Future[T]) = proc asyncCheck*[T](future: Future[T]) =
## Sets a callback on ``future`` which raises an exception if the future ## Sets a callback on ``future`` which raises an exception if the future
## finished with an error. ## finished with an error.

View file

@ -9,7 +9,7 @@
include "system/inclrtl" include "system/inclrtl"
import os, oids, tables, strutils, times, heapqueue, lists import os, oids, tables, strutils, times, heapqueue, lists, queues
import nativesockets, net, deques import nativesockets, net, deques
@ -1664,6 +1664,17 @@ proc accept*(socket: AsyncFD,
# -- Await Macro # -- Await Macro
include asyncmacro include asyncmacro
proc readAll*(future: FutureStream[string]): Future[string] {.async.} =
## Returns a future that will complete when all the string data from the
## specified future stream is retrieved.
result = ""
while true:
let (hasValue, value) = await future.read()
if hasValue:
result.add(value)
else:
break
proc recvLine*(socket: AsyncFD): Future[string] {.async.} = proc recvLine*(socket: AsyncFD): Future[string] {.async.} =
## Reads a line of data from ``socket``. Returned future will complete once ## Reads a line of data from ``socket``. Returned future will complete once
## a full line is read or an error occurs. ## a full line is read or an error occurs.

View file

@ -0,0 +1,53 @@
discard """
file: "tfuturestream.nim"
exitcode: 0
output: '''
0
1
2
3
4
5
Done
Finished
'''
"""
import asyncdispatch
var fs = newFutureStream[int]()
proc alpha() {.async.} =
for i in 0 .. 5:
await sleepAsync(1000)
await fs.write(i)
echo("Done")
fs.complete()
proc beta() {.async.} =
while not fs.finished:
let (hasValue, value) = await fs.read()
if hasValue:
echo(value)
echo("Finished")
asyncCheck alpha()
waitFor beta()
# TODO: Something like this should work eventually.
# proc delta(): FutureStream[string] {.async.} =
# for i in 0 .. 5:
# await sleepAsync(1000)
# result.put($i)
# return ""
# proc omega() {.async.} =
# let fut = delta()
# while not fut.finished():
# echo(await fs.takeAsync())
# echo("Finished")
# waitFor omega()

View file

@ -13,7 +13,9 @@ proc asyncTest() {.async.} =
var client = newAsyncHttpClient() var client = newAsyncHttpClient()
var resp = await client.request("http://example.com/") var resp = await client.request("http://example.com/")
doAssert(resp.code.is2xx) doAssert(resp.code.is2xx)
doAssert("<title>Example Domain</title>" in resp.body) var body = await resp.body
body = await resp.body # Test caching
doAssert("<title>Example Domain</title>" in body)
resp = await client.request("http://example.com/404") resp = await client.request("http://example.com/404")
doAssert(resp.code.is4xx) doAssert(resp.code.is4xx)
@ -47,7 +49,8 @@ proc asyncTest() {.async.} =
echo("Downloaded ", progress, " of ", total) echo("Downloaded ", progress, " of ", total)
echo("Current rate: ", speed div 1000, "kb/s") echo("Current rate: ", speed div 1000, "kb/s")
client.onProgressChanged = onProgressChanged client.onProgressChanged = onProgressChanged
discard await client.getContent("http://speedtest-ams2.digitalocean.com/100mb.test") await client.downloadFile("http://speedtest-ams2.digitalocean.com/100mb.test",
"100mb.test")
client.close() client.close()
@ -94,7 +97,8 @@ proc syncTest() =
echo("Downloaded ", progress, " of ", total) echo("Downloaded ", progress, " of ", total)
echo("Current rate: ", speed div 1000, "kb/s") echo("Current rate: ", speed div 1000, "kb/s")
client.onProgressChanged = onProgressChanged client.onProgressChanged = onProgressChanged
discard client.getContent("http://speedtest-ams2.digitalocean.com/100mb.test") client.downloadFile("http://speedtest-ams2.digitalocean.com/100mb.test",
"100mb.test")
client.close() client.close()

View file

@ -36,10 +36,7 @@ proc download(pkg: string; c: Controls) {.async.} =
c.bar.value = clamp(int(progress*100 div total), 0, 100) c.bar.value = clamp(int(progress*100 div total), 0, 100)
client.onProgressChanged = onProgressChanged client.onProgressChanged = onProgressChanged
# XXX give a destination filename instead await client.downloadFile("https://nim-lang.org/download/" & pkg & ".7z", z)
let contents = await client.getContent("https://nim-lang.org/download/" & pkg & ".7z")
# XXX make this async somehow:
writeFile(z, contents)
c.bar.value = 100 c.bar.value = 100
let p = osproc.startProcess("7zG.exe", getCurrentDir() / r"..\dist", let p = osproc.startProcess("7zG.exe", getCurrentDir() / r"..\dist",
["x", pkg & ".7z"]) ["x", pkg & ".7z"])