summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--changelog.md2
-rw-r--r--compiler/semasgn.nim2
-rw-r--r--lib/pure/asyncdispatch.nim2
-rw-r--r--lib/pure/asyncfutures.nim79
-rw-r--r--lib/pure/asyncmacro.nim15
-rw-r--r--tests/async/tasync_traceback.nim116
6 files changed, 199 insertions, 17 deletions
diff --git a/changelog.md b/changelog.md
index de4b2f251..1c5848ce8 100644
--- a/changelog.md
+++ b/changelog.md
@@ -111,6 +111,8 @@ This now needs to be written as:
 - The ``[]`` proc for strings now raises an ``IndexError`` exception when
   the specified slice is out of bounds. See issue
   [#6223](https://github.com/nim-lang/Nim/issues/6223) for more details.
+  You can use ``substr(str, start, finish)`` to get the old behaviour back,
+  see [this commit](https://github.com/nim-lang/nimbot/commit/98cc031a27ea89947daa7f0bb536bcf86462941f) for an example.
 - ``strutils.split`` and ``strutils.rsplit`` with an empty string and a
   separator now returns that empty string.
   See issue [#4377](https://github.com/nim-lang/Nim/issues/4377).
diff --git a/compiler/semasgn.nim b/compiler/semasgn.nim
index db08605cf..67af6ade7 100644
--- a/compiler/semasgn.nim
+++ b/compiler/semasgn.nim
@@ -261,7 +261,7 @@ proc addParam(procType: PType; param: PSym) =
   rawAddSon(procType, param.typ)
 
 proc liftBody(c: PContext; typ: PType; kind: TTypeAttachedOp;
-              info: TLineInfo): PSym {.discardable.} =
+              info: TLineInfo): PSym =
   var a: TLiftCtx
   a.info = info
   a.c = c
diff --git a/lib/pure/asyncdispatch.nim b/lib/pure/asyncdispatch.nim
index 23eb80b37..b62cf2e9b 100644
--- a/lib/pure/asyncdispatch.nim
+++ b/lib/pure/asyncdispatch.nim
@@ -1493,7 +1493,7 @@ proc poll*(timeout = 500) =
   ## Waits for completion events and processes them. Raises ``ValueError``
   ## if there are no pending operations. This runs the underlying OS
   ## `epoll`:idx: or `kqueue`:idx: primitive only once.
-  discard runOnce()
+  discard runOnce(timeout)
 
 # Common procedures between current and upcoming asyncdispatch
 include includes.asynccommon
diff --git a/lib/pure/asyncfutures.nim b/lib/pure/asyncfutures.nim
index 4bd3227a1..bcc3ab613 100644
--- a/lib/pure/asyncfutures.nim
+++ b/lib/pure/asyncfutures.nim
@@ -1,4 +1,4 @@
-import os, tables, strutils, times, heapqueue, options, deques
+import os, tables, strutils, times, heapqueue, options, deques, cstrutils
 
 # TODO: This shouldn't need to be included, but should ideally be exported.
 type
@@ -217,17 +217,78 @@ proc `callback=`*[T](future: Future[T],
   ## If future has already completed then ``cb`` will be called immediately.
   future.callback = proc () = cb(future)
 
+proc getHint(entry: StackTraceEntry): string =
+  ## We try to provide some hints about stack trace entries that the user
+  ## may not be familiar with, in particular calls inside the stdlib.
+  result = ""
+  if entry.procname == "processPendingCallbacks":
+    if cmpIgnoreStyle(entry.filename, "asyncdispatch.nim") == 0:
+      return "Executes pending callbacks"
+  elif entry.procname == "poll":
+    if cmpIgnoreStyle(entry.filename, "asyncdispatch.nim") == 0:
+      return "Processes asynchronous completion events"
+
+  if entry.procname.endsWith("_continue"):
+    if cmpIgnoreStyle(entry.filename, "asyncmacro.nim") == 0:
+      return "Resumes an async procedure"
+
+proc `$`*(entries: seq[StackTraceEntry]): string =
+  result = ""
+  # Find longest filename & line number combo for alignment purposes.
+  var longestLeft = 0
+  for entry in entries:
+    if entry.procName.isNil: continue
+
+    let left = $entry.filename & $entry.line
+    if left.len > longestLeft:
+      longestLeft = left.len
+
+  var indent = 2
+  # Format the entries.
+  for entry in entries:
+    if entry.procName.isNil:
+      if entry.line == -10:
+        result.add(spaces(indent) & "#[\n")
+        indent.inc(2)
+      else:
+        indent.dec(2)
+        result.add(spaces(indent)& "]#\n")
+      continue
+
+    let left = "$#($#)" % [$entry.filename, $entry.line]
+    result.add((spaces(indent) & "$#$# $#\n") % [
+      left,
+      spaces(longestLeft - left.len + 2),
+      $entry.procName
+    ])
+    let hint = getHint(entry)
+    if hint.len > 0:
+      result.add(spaces(indent+2) & "## " & hint & "\n")
+
 proc injectStacktrace[T](future: Future[T]) =
-  # TODO: Come up with something better.
   when not defined(release):
-    var msg = ""
-    msg.add("\n  " & future.fromProc & "'s lead up to read of failed Future:")
+    const header = "\nAsync traceback:\n"
 
-    if not future.errorStackTrace.isNil and future.errorStackTrace != "":
-      msg.add("\n" & indent(future.errorStackTrace.strip(), 4))
-    else:
-      msg.add("\n    Empty or nil stack trace.")
-    future.error.msg.add(msg)
+    var exceptionMsg = future.error.msg
+    if header in exceptionMsg:
+      # This is messy: extract the original exception message from the msg
+      # containing the async traceback.
+      let start = exceptionMsg.find(header)
+      exceptionMsg = exceptionMsg[0..<start]
+
+
+    var newMsg = exceptionMsg & header
+
+    let entries = getStackTraceEntries(future.error)
+    newMsg.add($entries)
+
+    newMsg.add("Exception message: " & exceptionMsg & "\n")
+    newMsg.add("Exception type:")
+
+    # # For debugging purposes
+    # for entry in getStackTraceEntries(future.error):
+    #   newMsg.add "\n" & $entry
+    future.error.msg = newMsg
 
 proc read*[T](future: Future[T] | FutureVar[T]): T =
   ## Retrieves the value of ``future``. Future must be finished otherwise
diff --git a/lib/pure/asyncmacro.nim b/lib/pure/asyncmacro.nim
index a8e378d5c..8c679929d 100644
--- a/lib/pure/asyncmacro.nim
+++ b/lib/pure/asyncmacro.nim
@@ -25,10 +25,10 @@ proc skipStmtList(node: NimNode): NimNode {.compileTime.} =
     result = node[0]
 
 template createCb(retFutureSym, iteratorNameSym,
-                  name, futureVarCompletions: untyped) =
+                  strName, identName, futureVarCompletions: untyped) =
   var nameIterVar = iteratorNameSym
   #{.push stackTrace: off.}
-  proc cb0 {.closure.} =
+  proc identName {.closure.} =
     try:
       if not nameIterVar.finished:
         var next = nameIterVar()
@@ -36,11 +36,11 @@ template createCb(retFutureSym, iteratorNameSym,
           if not retFutureSym.finished:
             let msg = "Async procedure ($1) yielded `nil`, are you await'ing a " &
                     "`nil` Future?"
-            raise newException(AssertionError, msg % name)
+            raise newException(AssertionError, msg % strName)
         else:
           {.gcsafe.}:
             {.push hint[ConvFromXtoItselfNotNeeded]: off.}
-            next.callback = (proc() {.closure, gcsafe.})(cb0)
+            next.callback = (proc() {.closure, gcsafe.})(identName)
             {.pop.}
     except:
       futureVarCompletions
@@ -52,7 +52,7 @@ template createCb(retFutureSym, iteratorNameSym,
       else:
         retFutureSym.fail(getCurrentException())
 
-  cb0()
+  identName()
   #{.pop.}
 proc generateExceptionCheck(futSym,
     tryStmt, rootReceiver, fromNode: NimNode): NimNode {.compileTime.} =
@@ -389,9 +389,12 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
     outerProcBody.add(closureIterator)
 
     # -> createCb(retFuture)
-    #var cbName = newIdentNode("cb")
+    # NOTE: The "_continue" suffix is checked for in asyncfutures.nim to produce
+    # friendlier stack traces:
+    var cbName = genSym(nskProc, prcName & "_continue")
     var procCb = getAst createCb(retFutureSym, iteratorNameSym,
                          newStrLitNode(prcName),
+                         cbName,
                          createFutureVarCompletions(futureVarIdents, nil))
     outerProcBody.add procCb
 
diff --git a/tests/async/tasync_traceback.nim b/tests/async/tasync_traceback.nim
new file mode 100644
index 000000000..08f7e7317
--- /dev/null
+++ b/tests/async/tasync_traceback.nim
@@ -0,0 +1,116 @@
+discard """
+  exitcode: 0
+  disabled: "windows"
+  output: '''
+b failure
+Async traceback:
+  tasync_traceback.nim(97) tasync_traceback
+  asyncmacro.nim(395)      a
+  asyncmacro.nim(34)       a_continue
+    ## Resumes an async procedure
+  tasync_traceback.nim(95) aIter
+  asyncmacro.nim(395)      b
+  asyncmacro.nim(34)       b_continue
+    ## Resumes an async procedure
+  tasync_traceback.nim(92) bIter
+  #[
+    tasync_traceback.nim(97) tasync_traceback
+    asyncmacro.nim(395)      a
+    asyncmacro.nim(43)       a_continue
+      ## Resumes an async procedure
+    asyncfutures.nim(211)    callback=
+    asyncfutures.nim(190)    addCallback
+    asyncfutures.nim(53)     callSoon
+    asyncmacro.nim(34)       a_continue
+      ## Resumes an async procedure
+    asyncmacro.nim(0)        aIter
+    asyncfutures.nim(304)    read
+  ]#
+Exception message: b failure
+Exception type:
+
+bar failure
+Async traceback:
+  tasync_traceback.nim(113) tasync_traceback
+  asyncdispatch.nim(1492)   waitFor
+  asyncdispatch.nim(1496)   poll
+    ## Processes asynchronous completion events
+  asyncdispatch.nim(1262)   runOnce
+  asyncdispatch.nim(183)    processPendingCallbacks
+    ## Executes pending callbacks
+  asyncmacro.nim(34)        bar_continue
+    ## Resumes an async procedure
+  tasync_traceback.nim(108) barIter
+  #[
+    tasync_traceback.nim(113) tasync_traceback
+    asyncdispatch.nim(1492)   waitFor
+    asyncdispatch.nim(1496)   poll
+      ## Processes asynchronous completion events
+    asyncdispatch.nim(1262)   runOnce
+    asyncdispatch.nim(183)    processPendingCallbacks
+      ## Executes pending callbacks
+    asyncmacro.nim(34)        foo_continue
+      ## Resumes an async procedure
+    asyncmacro.nim(0)         fooIter
+    asyncfutures.nim(304)     read
+  ]#
+Exception message: bar failure
+Exception type:'''
+"""
+import asyncdispatch
+
+# Tests to ensure our exception trace backs are friendly.
+
+# --- Simple test. ---
+#
+# What does this look like when it's synchronous?
+#
+# tasync_traceback.nim(23) tasync_traceback
+# tasync_traceback.nim(21) a
+# tasync_traceback.nim(18) b
+# Error: unhandled exception: b failure [OSError]
+#
+# Good (not quite ideal, but gotta work within constraints) traceback,
+# when exception is unhandled:
+#
+# <traceback for the unhandled exception>
+# <very much a bunch of noise>
+# <would be ideal to customise this>
+# <(the code responsible is in excpt:raiseExceptionAux)>
+# Error: unhandled exception: b failure
+# ===============
+# Async traceback
+# ===============
+#
+# tasync_traceback.nim(23) tasync_traceback
+#
+# tasync_traceback.nim(21) a
+# tasync_traceback.nim(18) b
+
+proc b(): Future[int] {.async.} =
+  if true:
+    raise newException(OSError, "b failure")
+
+proc a(): Future[int] {.async.} =
+  return await b()
+
+let aFut = a()
+try:
+  discard waitFor aFut
+except Exception as exc:
+  echo exc.msg
+echo()
+
+# From #6803
+proc bar(): Future[string] {.async.} =
+  await sleepAsync(100)
+  if true:
+    raise newException(OSError, "bar failure")
+
+proc foo(): Future[string] {.async.} = return await bar()
+
+try:
+  echo waitFor(foo())
+except Exception as exc:
+  echo exc.msg
+echo()
\ No newline at end of file
nload can be checked ## by specifying a ``onProgressChanged`` procedure to the ``store`` or ## ``retrFile`` procedures. ## ## .. code-block::nim ## import asyncdispatch, asyncftpclient ## ## proc onProgressChanged(total, progress: BiggestInt, ## speed: float): Future[void] = ## echo("Uploaded ", progress, " of ", total, " bytes") ## echo("Current speed: ", speed, " kb/s") ## ## proc main() {.async.} = ## var ftp = newAsyncFtpClient("example.com", user = "test", pass = "test") ## await ftp.connect() ## await ftp.store("file.txt", "/home/user/file.txt", onProgressChanged) ## echo("File finished uploading") ## waitFor(main()) import asyncdispatch, asyncnet, nativesockets, strutils, parseutils, os, times from net import BufferSize type AsyncFtpClient* = ref object csock*: AsyncSocket dsock*: AsyncSocket user*, pass*: string address*: string port*: Port jobInProgress*: bool job*: FtpJob dsockConnected*: bool FtpJobType* = enum JRetrText, JRetr, JStore FtpJob = ref object prc: proc (ftp: AsyncFtpClient, async: bool): bool {.nimcall, gcsafe.} case typ*: FtpJobType of JRetrText: lines: string of JRetr, JStore: file: File filename: string total: BiggestInt # In bytes. progress: BiggestInt # In bytes. oneSecond: BiggestInt # Bytes transferred in one second. lastProgressReport: float # Time toStore: string # Data left to upload (Only used with async) FtpEventType* = enum EvTransferProgress, EvLines, EvRetr, EvStore FtpEvent* = object ## Event filename*: string case typ*: FtpEventType of EvLines: lines*: string ## Lines that have been transferred. of EvRetr, EvStore: ## Retr/Store operation finished. nil of EvTransferProgress: bytesTotal*: BiggestInt ## Bytes total. bytesFinished*: BiggestInt ## Bytes transferred. speed*: BiggestInt ## Speed in bytes/s currentJob*: FtpJobType ## The current job being performed. ReplyError* = object of IOError ProgressChangedProc* = proc (total, progress: BiggestInt, speed: float): Future[void] {.closure, gcsafe.} const multiLineLimit = 10000 proc expectReply(ftp: AsyncFtpClient): Future[TaintedString] {.async.} = var line = await ftp.csock.recvLine() result = TaintedString(line) var count = 0 while line.len > 3 and line[3] == '-': ## Multi-line reply. line = await ftp.csock.recvLine() string(result).add("\n" & line) count.inc() if count >= multiLineLimit: raise newException(ReplyError, "Reached maximum multi-line reply count.") proc send*(ftp: AsyncFtpClient, m: string): Future[TaintedString] {.async.} = ## Send a message to the server, and wait for a primary reply. ## ``\c\L`` is added for you. ## ## You need to make sure that the message ``m`` doesn't contain any newline ## characters. Failing to do so will raise ``AssertionDefect``. ## ## **Note:** The server may return multiple lines of coded replies. doAssert(not m.contains({'\c', '\L'}), "message shouldn't contain any newline characters") await ftp.csock.send(m & "\c\L") return await ftp.expectReply() proc assertReply(received: TaintedString, expected: varargs[string]) = for i in items(expected): if received.string.startsWith(i): return raise newException(ReplyError, "Expected reply '$1' got: $2" % [expected.join("' or '"), received.string]) proc pasv(ftp: AsyncFtpClient) {.async.} = ## Negotiate a data connection. ftp.dsock = newAsyncSocket() var pasvMsg = (await ftp.send("PASV")).string.strip.TaintedString assertReply(pasvMsg, "227") var betweenParens = captureBetween(pasvMsg.string, '(', ')') var nums = betweenParens.split(',') var ip = nums[0 .. ^3] var port = nums[^2 .. ^1] var properPort = port[0].parseInt()*256+port[1].parseInt() await ftp.dsock.connect(ip.join("."), Port(properPort)) ftp.dsockConnected = true proc normalizePathSep(path: string): string = return replace(path, '\\', '/') proc connect*(ftp: AsyncFtpClient) {.async.} = ## Connect to the FTP server specified by ``ftp``. await ftp.csock.connect(ftp.address, ftp.port) var reply = await ftp.expectReply() if string(reply).startsWith("120"): # 120 Service ready in nnn minutes. # We wait until we receive 220. reply = await ftp.expectReply() # Handle 220 messages from the server assertReply(reply, "220") if ftp.user != "": assertReply(await(ftp.send("USER " & ftp.user)), "230", "331") if ftp.pass != "": assertReply(await(ftp.send("PASS " & ftp.pass)), "230") proc pwd*(ftp: AsyncFtpClient): Future[TaintedString] {.async.} = ## Returns the current working directory. let wd = await ftp.send("PWD") assertReply wd, "257" return wd.string.captureBetween('"').TaintedString # " proc cd*(ftp: AsyncFtpClient, dir: string) {.async.} = ## Changes the current directory on the remote FTP server to ``dir``. assertReply(await(ftp.send("CWD " & dir.normalizePathSep)), "250") proc cdup*(ftp: AsyncFtpClient) {.async.} = ## Changes the current directory to the parent of the current directory. assertReply(await(ftp.send("CDUP")), "200") proc getLines(ftp: AsyncFtpClient): Future[string] {.async.} = ## Downloads text data in ASCII mode result = "" assert ftp.dsockConnected while ftp.dsockConnected: let r = await ftp.dsock.recvLine() if r.string == "": ftp.dsockConnected = false else: result.add(r.string & "\n") assertReply(await(ftp.expectReply()), "226") proc listDirs*(ftp: AsyncFtpClient, dir = ""): Future[seq[string]] {.async.} = ## Returns a list of filenames in the given directory. If ``dir`` is "", ## the current directory is used. If ``async`` is true, this ## function will return immediately and it will be your job to ## use asyncdispatch's ``poll`` to progress this operation. await ftp.pasv() assertReply(await(ftp.send("NLST " & dir.normalizePathSep)), ["125", "150"]) result = splitLines(await ftp.getLines()) proc fileExists*(ftp: AsyncFtpClient, file: string): Future[bool] {.async.} = ## Determines whether ``file`` exists. var files = await ftp.listDirs() for f in items(files): if f.normalizePathSep == file.normalizePathSep: return true proc createDir*(ftp: AsyncFtpClient, dir: string, recursive = false){.async.} = ## Creates a directory ``dir``. If ``recursive`` is true, the topmost ## subdirectory of ``dir`` will be created first, following the secondmost... ## etc. this allows you to give a full path as the ``dir`` without worrying ## about subdirectories not existing. if not recursive: assertReply(await(ftp.send("MKD " & dir.normalizePathSep)), "257") else: var reply = TaintedString"" var previousDirs = "" for p in split(dir, {os.DirSep, os.AltSep}): if p != "": previousDirs.add(p) reply = await ftp.send("MKD " & previousDirs) previousDirs.add('/') assertReply reply, "257" proc chmod*(ftp: AsyncFtpClient, path: string, permissions: set[FilePermission]) {.async.} = ## Changes permission of ``path`` to ``permissions``. var userOctal = 0 var groupOctal = 0 var otherOctal = 0 for i in items(permissions): case i of fpUserExec: userOctal.inc(1) of fpUserWrite: userOctal.inc(2) of fpUserRead: userOctal.inc(4) of fpGroupExec: groupOctal.inc(1) of fpGroupWrite: groupOctal.inc(2) of fpGroupRead: groupOctal.inc(4) of fpOthersExec: otherOctal.inc(1) of fpOthersWrite: otherOctal.inc(2) of fpOthersRead: otherOctal.inc(4) var perm = $userOctal & $groupOctal & $otherOctal assertReply(await(ftp.send("SITE CHMOD " & perm & " " & path.normalizePathSep)), "200") proc list*(ftp: AsyncFtpClient, dir = ""): Future[string] {.async.} = ## Lists all files in ``dir``. If ``dir`` is ``""``, uses the current ## working directory. await ftp.pasv() let reply = await ftp.send("LIST" & " " & dir.normalizePathSep) assertReply(reply, ["125", "150"]) result = await ftp.getLines() proc retrText*(ftp: AsyncFtpClient, file: string): Future[string] {.async.} = ## Retrieves ``file``. File must be ASCII text. await ftp.pasv() let reply = await ftp.send("RETR " & file.normalizePathSep) assertReply(reply, ["125", "150"]) result = await ftp.getLines() proc getFile(ftp: AsyncFtpClient, file: File, total: BiggestInt, onProgressChanged: ProgressChangedProc) {.async.} = assert ftp.dsockConnected var progress = 0 var progressInSecond = 0 var countdownFut = sleepAsync(1000) var dataFut = ftp.dsock.recv(BufferSize) while ftp.dsockConnected: await dataFut or countdownFut if countdownFut.finished: asyncCheck onProgressChanged(total, progress, progressInSecond.float) progressInSecond = 0 countdownFut = sleepAsync(1000) if dataFut.finished: let data = dataFut.read if data != "": progress.inc(data.len) progressInSecond.inc(data.len) file.write(data) dataFut = ftp.dsock.recv(BufferSize) else: ftp.dsockConnected = false assertReply(await(ftp.expectReply()), "226") proc defaultOnProgressChanged*(total, progress: BiggestInt, speed: float): Future[void] {.nimcall, gcsafe.} = ## Default FTP ``onProgressChanged`` handler. Does nothing. result = newFuture[void]() #echo(total, " ", progress, " ", speed) result.complete() proc retrFile*(ftp: AsyncFtpClient, file, dest: string, onProgressChanged: ProgressChangedProc = defaultOnProgressChanged) {.async.} = ## Downloads ``file`` and saves it to ``dest``. ## The ``EvRetr`` event is passed to the specified ``handleEvent`` function ## when the download is finished. The event's ``filename`` field will be equal ## to ``file``. var destFile = open(dest, mode = fmWrite) await ftp.pasv() var reply = await ftp.send("RETR " & file.normalizePathSep) assertReply reply, ["125", "150"] if {'(', ')'} notin reply.string: raise newException(ReplyError, "Reply has no file size.") var fileSize: BiggestInt if reply.string.captureBetween('(', ')').parseBiggestInt(fileSize) == 0: raise newException(ReplyError, "Reply has no file size.") await getFile(ftp, destFile, fileSize, onProgressChanged) destFile.close() proc doUpload(ftp: AsyncFtpClient, file: File, onProgressChanged: ProgressChangedProc) {.async.} = assert ftp.dsockConnected let total = file.getFileSize() var data = newString(4000) var progress = 0 var progressInSecond = 0 var countdownFut = sleepAsync(1000) var sendFut: Future[void] = nil while ftp.dsockConnected: if sendFut == nil or sendFut.finished: # TODO: Async file reading. let len = file.readBuffer(addr(data[0]), 4000) setLen(data, len) if len == 0: # File finished uploading. ftp.dsock.close() ftp.dsockConnected = false assertReply(await(ftp.expectReply()), "226") else: progress.inc(len) progressInSecond.inc(len) sendFut = ftp.dsock.send(data) if countdownFut.finished: asyncCheck onProgressChanged(total, progress, progressInSecond.float) progressInSecond = 0 countdownFut = sleepAsync(1000) await countdownFut or sendFut proc store*(ftp: AsyncFtpClient, file, dest: string, onProgressChanged: ProgressChangedProc = defaultOnProgressChanged) {.async.} = ## Uploads ``file`` to ``dest`` on the remote FTP server. Usage of this ## function asynchronously is recommended to view the progress of ## the download. ## The ``EvStore`` event is passed to the specified ``handleEvent`` function ## when the upload is finished, and the ``filename`` field will be ## equal to ``file``. var destFile = open(file) await ftp.pasv() let reply = await ftp.send("STOR " & dest.normalizePathSep) assertReply reply, ["125", "150"] await doUpload(ftp, destFile, onProgressChanged) proc rename*(ftp: AsyncFtpClient, nameFrom: string, nameTo: string) {.async.} = ## Rename a file or directory on the remote FTP Server from current name ## ``name_from`` to new name ``name_to`` assertReply(await ftp.send("RNFR " & nameFrom), "350") assertReply(await ftp.send("RNTO " & nameTo), "250") proc removeFile*(ftp: AsyncFtpClient, filename: string) {.async.} = ## Delete a file ``filename`` on the remote FTP server assertReply(await ftp.send("DELE " & filename), "250") proc removeDir*(ftp: AsyncFtpClient, dir: string) {.async.} = ## Delete a directory ``dir`` on the remote FTP server assertReply(await ftp.send("RMD " & dir), "250") proc newAsyncFtpClient*(address: string, port = Port(21), user, pass = ""): AsyncFtpClient = ## Creates a new ``AsyncFtpClient`` object. new result result.user = user result.pass = pass result.address = address result.port = port result.dsockConnected = false result.csock = newAsyncSocket() when not defined(testing) and isMainModule: var ftp = newAsyncFtpClient("example.com", user = "test", pass = "test") proc main(ftp: AsyncFtpClient) {.async.} = await ftp.connect() echo await ftp.pwd() echo await ftp.listDirs() await ftp.store("payload.jpg", "payload.jpg") await ftp.retrFile("payload.jpg", "payload2.jpg") await ftp.rename("payload.jpg", "payload_renamed.jpg") await ftp.store("payload.jpg", "payload_remove.jpg") await ftp.removeFile("payload_remove.jpg") await ftp.createDir("deleteme") await ftp.removeDir("deleteme") echo("Finished") waitFor main(ftp)