about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config/mailcap.nim15
-rw-r--r--src/io/dynstream.nim28
-rw-r--r--src/io/stdio.nim25
-rw-r--r--src/loader/loader.nim1
-rw-r--r--src/local/client.nim129
-rw-r--r--src/local/pager.nim343
-rw-r--r--src/server/forkserver.nim1
7 files changed, 262 insertions, 280 deletions
diff --git a/src/config/mailcap.nim b/src/config/mailcap.nim
index d1c103b5..8da44955 100644
--- a/src/config/mailcap.nim
+++ b/src/config/mailcap.nim
@@ -306,16 +306,14 @@ proc unquoteCommand*(ecmd, contentType, outpath: string; url: URL): string =
   var canpipe: bool
   return unquoteCommand(ecmd, contentType, outpath, url, canpipe)
 
-proc getMailcapEntry*(mailcap: Mailcap; contentType, outpath: string; url: URL):
-    ptr MailcapEntry =
+proc findMailcapEntry*(mailcap: var Mailcap; contentType, outpath: string;
+    url: URL): int =
   let mt = contentType.until('/')
-  if mt.len + 1 >= contentType.len:
-    return nil
   let st = contentType.until(AsciiWhitespace + {';'}, mt.len + 1)
-  for entry in mailcap:
-    if not (entry.mt.len == 1 and entry.mt[0] == '*') and entry.mt != mt:
+  for i, entry in mailcap.mpairs:
+    if entry.mt != "*" and not entry.mt.equalsIgnoreCase(mt):
       continue
-    if not (entry.subt.len == 1 and entry.subt[0] == '*') and entry.subt != st:
+    if entry.subt != "*" and not entry.subt.equalsIgnoreCase(st):
       continue
     if entry.test != "":
       var canpipe = true
@@ -324,4 +322,5 @@ proc getMailcapEntry*(mailcap: Mailcap; contentType, outpath: string; url: URL):
         continue
       if execCmd(cmd) != 0:
         continue
-    return unsafeAddr entry
+    return i
+  return -1
diff --git a/src/io/dynstream.nim b/src/io/dynstream.nim
index 4138f153..56faf202 100644
--- a/src/io/dynstream.nim
+++ b/src/io/dynstream.nim
@@ -163,6 +163,34 @@ method sclose*(s: PosixStream) =
   discard close(s.fd)
   s.closed = true
 
+proc closeHandle(fd, flags: cint) =
+  let devnull = open("/dev/null", flags)
+  doAssert devnull != -1
+  if devnull != fd:
+    discard dup2(devnull, fd)
+    discard close(devnull)
+
+proc closeStdin*() =
+  closeHandle(0, O_RDONLY)
+
+proc closeStdout*() =
+  closeHandle(1, O_WRONLY)
+
+proc closeStderr*() =
+  closeHandle(2, O_WRONLY)
+
+# When closing, ensure that no standard input stream ends up without a
+# handle to write to.
+#TODO do we really need this? I'm pretty sure I dup2 to every stream on
+# fork in all processes...
+proc safeClose*(ps: PosixStream) =
+  if ps.fd == 0:
+    closeStdin()
+  elif ps.fd == 1 or ps.fd == 2:
+    closeHandle(ps.fd, O_WRONLY)
+  else:
+    ps.sclose()
+
 proc newPosixStream*(fd: FileHandle): PosixStream =
   return PosixStream(fd: cint(fd), blocking: true)
 
diff --git a/src/io/stdio.nim b/src/io/stdio.nim
deleted file mode 100644
index 729b50f6..00000000
--- a/src/io/stdio.nim
+++ /dev/null
@@ -1,25 +0,0 @@
-import std/posix
-
-proc closeHandle(fd, flags: cint) =
-  let devnull = open("/dev/null", flags)
-  doAssert devnull != -1
-  if devnull != fd:
-    discard dup2(devnull, fd)
-    discard close(devnull)
-
-proc closeStdin*() =
-  closeHandle(0, O_RDONLY)
-
-proc closeStdout*() =
-  closeHandle(1, O_WRONLY)
-
-proc closeStderr*() =
-  closeHandle(2, O_WRONLY)
-
-proc safeClose*(fd: cint) =
-  if fd == 0:
-    closeStdin()
-  elif fd == 1 or fd == 2:
-    closeHandle(fd, O_WRONLY)
-  else:
-    discard close(fd)
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index 54ecb77d..64fc6899 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -34,7 +34,6 @@ import io/bufwriter
 import io/dynstream
 import io/poll
 import io/serversocket
-import io/stdio
 import io/tempfile
 import io/urlfilter
 import loader/connecterror
diff --git a/src/local/client.nim b/src/local/client.nim
index 01688f9e..e090f0f4 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -57,7 +57,6 @@ type
   Client* = ref object of Window
     alive: bool
     config {.jsget.}: Config
-    consoleWrapper: ConsoleWrapper
     feednext: bool
     pager {.jsget.}: Pager
     pressed: tuple[col, row: int]
@@ -67,14 +66,6 @@ type
   ContainerData = ref object of MapData
     container: Container
 
-  ConsoleWrapper = object
-    console: Console
-    container: Container
-    prev: Container
-
-func console(client: Client): Console {.jsfget.} =
-  return client.consoleWrapper.console
-
 template pollData(client: Client): PollData =
   client.pager.pollData
 
@@ -84,6 +75,12 @@ template forkserver(client: Client): ForkServer =
 template readChar(client: Client): char =
   client.pager.term.readChar()
 
+template consoleWrapper(client: Client): ConsoleWrapper =
+  client.pager.consoleWrapper
+
+func console(client: Client): Console {.jsfget.} =
+  return client.consoleWrapper.console
+
 proc interruptHandler(rt: JSRuntime; opaque: pointer): cint {.cdecl.} =
   let client = cast[Client](opaque)
   if client.console == nil or client.pager.term.istream == nil:
@@ -363,16 +360,6 @@ proc input(client: Client): EmptyPromise =
     p.resolve()
   return p
 
-proc showConsole(client: Client) {.jsfunc.} =
-  let container = client.consoleWrapper.container
-  if client.pager.container != container:
-    client.consoleWrapper.prev = client.pager.container
-    client.pager.setContainer(container)
-
-proc hideConsole(client: Client) {.jsfunc.} =
-  if client.pager.container == client.consoleWrapper.container:
-    client.pager.setContainer(client.consoleWrapper.prev)
-
 proc consoleBuffer(client: Client): Container {.jsfget.} =
   return client.consoleWrapper.container
 
@@ -411,33 +398,34 @@ proc acceptBuffers(client: Client) =
     let key = pager.addLoaderClient(container.process, container.loaderConfig,
       container.clonedFrom)
     let loader = pager.loader
-    stream.withPacketWriter w:
-      w.swrite(key)
-      if item.fdin != -1:
-        let outputId = item.istreamOutputId
-        if container.cacheId == -1:
-          container.cacheId = loader.addCacheFile(outputId, loader.clientPid)
-        if container.request.url.scheme == "cache":
-          # loading from cache; now both the buffer and us hold a new reference
-          # to the cached item, but it's only shared with the buffer. add a
-          # pager ref too.
-          loader.shareCachedItem(container.cacheId, loader.clientPid)
-        var outCacheId = container.cacheId
-        let pid = container.process
-        if item.fdout == item.fdin:
-          loader.shareCachedItem(container.cacheId, pid)
-          loader.resume(item.istreamOutputId)
-        else:
-          outCacheId = loader.addCacheFile(item.ostreamOutputId, pid)
-          loader.resume([item.istreamOutputId, item.ostreamOutputId])
+    if item.istreamOutputId != -1: # new buffer
+      if container.cacheId == -1:
+        container.cacheId = loader.addCacheFile(item.istreamOutputId,
+          loader.clientPid)
+      if container.request.url.scheme == "cache":
+        # loading from cache; now both the buffer and us hold a new reference
+        # to the cached item, but it's only shared with the buffer. add a
+        # pager ref too.
+        loader.shareCachedItem(container.cacheId, loader.clientPid)
+      let pid = container.process
+      var outCacheId = container.cacheId
+      if not item.redirected:
+        loader.shareCachedItem(container.cacheId, pid)
+        loader.resume(item.istreamOutputId)
+      else:
+        outCacheId = loader.addCacheFile(item.ostreamOutputId, pid)
+        loader.resume([item.istreamOutputId, item.ostreamOutputId])
+      stream.withPacketWriter w:
+        w.swrite(key)
         w.swrite(outCacheId)
-    if item.fdin != -1:
-      # pass down fdout
+      # pass down ostream
       # must come after the previous block so the first packet is flushed
-      stream.sendFileHandle(item.fdout)
-      discard close(item.fdout)
+      stream.sendFileHandle(FileHandle(item.ostream.fd))
+      item.ostream.sclose()
       container.setStream(stream, registerFun)
-    else:
+    else: # cloned buffer
+      stream.withPacketWriter w:
+        w.swrite(key)
       # buffer is cloned, just share the parent's cached source
       loader.shareCachedItem(container.cacheId, container.process)
       # also add a reference here; it will be removed when the container is
@@ -516,11 +504,7 @@ proc handleWrite(client: Client; fd: int) =
     client.pollData.register(fd, POLLIN)
 
 proc flushConsole*(client: Client) {.jsfunc.} =
-  if client.console == nil:
-    # hack for when client crashes before console has been initialized
-    client.consoleWrapper = ConsoleWrapper(
-      console: newConsole(newDynFileStream(stderr))
-    )
+  client.pager.flushConsole()
   client.handleRead(client.forkserver.estream.fd)
 
 proc handleError(client: Client; fd: int) =
@@ -544,14 +528,14 @@ proc handleError(client: Client; fd: int) =
       client.pollData.unregister(fd)
       client.loader.unset(fd)
       doAssert client.consoleWrapper.container != nil
-      client.showConsole()
+      client.pager.showConsole()
     else:
       discard client.loader.onError(fd) #TODO handle connection error?
   elif fd in client.loader.unregistered:
     discard # already unregistered...
   else:
     doAssert client.consoleWrapper.container != nil
-    client.showConsole()
+    client.pager.showConsole()
 
 let SIGWINCH {.importc, header: "<signal.h>", nodecl.}: cint
 
@@ -684,40 +668,6 @@ proc readFile(client: Client; path: string): string {.jsfunc.} =
 proc writeFile(client: Client; path, content: string) {.jsfunc.} =
   writeFile(path, content)
 
-const ConsoleTitle = "Browser Console"
-
-proc addConsole(pager: Pager; interactive: bool; clearFun, showFun, hideFun:
-    proc()): ConsoleWrapper =
-  if interactive and pager.config.start.console_buffer:
-    var pipefd: array[0..1, cint]
-    if pipe(pipefd) == -1:
-      raise newException(Defect, "Failed to open console pipe.")
-    let url = newURL("stream:console").get
-    let container = pager.readPipe0("text/plain", CHARSET_UNKNOWN, pipefd[0],
-      url, ConsoleTitle, {})
-    let err = newPosixStream(pipefd[1])
-    err.write("Type (M-c) console.hide() to return to buffer mode.\n")
-    let console = newConsole(err, clearFun, showFun, hideFun)
-    return ConsoleWrapper(console: console, container: container)
-  else:
-    let err = newPosixStream(stderr.getFileHandle())
-    return ConsoleWrapper(console: newConsole(err))
-
-proc clearConsole(client: Client) =
-  var pipefd: array[0..1, cint]
-  if pipe(pipefd) == -1:
-    raise newException(Defect, "Failed to open console pipe.")
-  let url = newURL("stream:console").get
-  let pager = client.pager
-  let replacement = pager.readPipe0("text/plain", CHARSET_UNKNOWN, pipefd[0],
-    url, ConsoleTitle, {})
-  replacement.replace = client.consoleWrapper.container
-  pager.replace(client.consoleWrapper.container, replacement)
-  client.consoleWrapper.container = replacement
-  let console = client.consoleWrapper.console
-  console.err.sclose()
-  console.err = newPosixStream(pipefd[1])
-
 proc dumpBuffers(client: Client) =
   client.headlessLoop()
   for container in client.pager.containers:
@@ -751,14 +701,6 @@ proc launchClient*(client: Client; pages: seq[string];
   client.loader.unregisterFun = proc(fd: int) =
     pager.pollData.unregister(fd)
   pager.launchPager(istream)
-  let clearFun = proc() =
-    client.clearConsole()
-  let showFun = proc() =
-    client.showConsole()
-  let hideFun = proc() =
-    client.hideConsole()
-  client.consoleWrapper = pager.addConsole(interactive = istream != nil,
-    clearFun, showFun, hideFun)
   client.timeouts = newTimeoutState(client.jsctx, evalJSFree2, client)
   client.pager.timeouts = client.timeouts
   addExitProc((proc() = client.cleanup()))
@@ -773,7 +715,8 @@ proc launchClient*(client: Client; pages: seq[string];
   if not stdin.isatty():
     # stdin may very well receive ANSI text
     let contentType = contentType.get("text/x-ansi")
-    client.pager.readPipe(contentType, cs, stdin.getFileHandle(), "*stdin*")
+    let ps = newPosixStream(STDIN_FILENO)
+    client.pager.readPipe(contentType, cs, ps, "*stdin*")
   for page in pages:
     client.pager.loadURL(page, ctype = contentType, cs = cs)
   client.pager.showAlerts()
diff --git a/src/local/pager.nim b/src/local/pager.nim
index c3e1a8d1..d4999d63 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -15,9 +15,9 @@ import io/bufreader
 import io/dynstream
 import io/poll
 import io/promise
-import io/stdio
 import io/tempfile
 import io/urlfilter
+import js/console
 import js/timeout
 import layout/renderdocument
 import loader/connecterror
@@ -71,14 +71,12 @@ type
     lmBufferFile = "(Upload)Filename: "
     lmAlert = "Alert: "
 
-  # fdin is the original fd; fdout may be the same, or different if mailcap
-  # is used.
   ProcMapItem = object
     container*: Container
-    fdin*: FileHandle
-    fdout*: FileHandle
+    ostream*: PosixStream
     istreamOutputId*: int
     ostreamOutputId*: int
+    redirected*: bool
 
   PagerAlertState = enum
     pasNormal, pasAlertOn, pasLoadInfo
@@ -115,6 +113,11 @@ type
     redraw: bool
     grid: FixedGrid
 
+  ConsoleWrapper* = object
+    console*: Console
+    container*: Container
+    prev*: Container
+
   Pager* = ref object
     alertState: PagerAlertState
     alerts*: seq[string]
@@ -124,6 +127,7 @@ type
     askprompt: string
     commandMode {.jsget.}: bool
     config*: Config
+    consoleWrapper*: ConsoleWrapper
     container*: Container
     cookiejars: Table[string, CookieJar]
     devRandom: PosixStream
@@ -159,6 +163,7 @@ type
 jsDestructor(Pager)
 
 # Forward declarations
+proc addConsole(pager: Pager; interactive: bool): ConsoleWrapper
 proc alert*(pager: Pager; msg: string)
 proc getLineHist(pager: Pager; mode: LineMode): LineHistory
 
@@ -376,6 +381,7 @@ proc launchPager*(pager: Pager; istream: PosixStream) =
     pager.alert("Failed to query DA1, please set display.query-da1 = false")
   pager.clearDisplay()
   pager.clearStatus()
+  pager.consoleWrapper = pager.addConsole(interactive = istream != nil)
 
 proc buffer(pager: Pager): Container {.jsfget, inline.} =
   return pager.container
@@ -868,8 +874,6 @@ proc dupeBuffer(pager: Pager; container: Container; url: URL) =
         pager.addContainer(container)
         pager.procmap.add(ProcMapItem(
           container: container,
-          fdin: -1,
-          fdout: -1,
           istreamOutputId: -1,
           ostreamOutputId: -1
         ))
@@ -1433,12 +1437,19 @@ proc loadURL*(pager: Pager; url: string; ctype = none(string);
     if container != nil:
       container.retry = urls
 
+proc createPipe(pager: Pager): (PosixStream, PosixStream) =
+  var pipefds {.noinit.}: array[2, cint]
+  if pipe(pipefds) == -1:
+    pager.alert("Failed to create pipe")
+    return (nil, nil)
+  return (newPosixStream(pipefds[0]), newPosixStream(pipefds[1]))
+
 proc readPipe0*(pager: Pager; contentType: string; cs: Charset;
-    fd: FileHandle; url: URL; title: string; flags: set[ContainerFlag]):
+    ps: PosixStream; url: URL; title: string; flags: set[ContainerFlag]):
     Container =
   var url = url
-  pager.loader.passFd(url.pathname, fd)
-  safeClose(fd)
+  pager.loader.passFd(url.pathname, FileHandle(ps.fd))
+  ps.safeClose()
   var loaderConfig: LoaderClientConfig
   var ourl: URL
   let bufferConfig = pager.applySiteconf(url, cs, loaderConfig, ourl)
@@ -1451,14 +1462,65 @@ proc readPipe0*(pager: Pager; contentType: string; cs: Charset;
     contentType = some(contentType)
   )
 
-proc readPipe*(pager: Pager; contentType: string; cs: Charset; fd: FileHandle;
+proc readPipe*(pager: Pager; contentType: string; cs: Charset; ps: PosixStream;
     title: string) =
   let url = newURL("stream:-").get
-  let container = pager.readPipe0(contentType, cs, fd, url, title,
+  let container = pager.readPipe0(contentType, cs, ps, url, title,
     {cfCanReinterpret, cfUserRequested})
   inc pager.numload
   pager.addContainer(container)
 
+const ConsoleTitle = "Browser Console"
+
+proc showConsole*(pager: Pager) =
+  let container = pager.consoleWrapper.container
+  if pager.container != container:
+    pager.consoleWrapper.prev = pager.container
+    pager.setContainer(container)
+
+proc hideConsole(pager: Pager) =
+  if pager.container == pager.consoleWrapper.container:
+    pager.setContainer(pager.consoleWrapper.prev)
+
+proc clearConsole(pager: Pager) =
+  let (pins, pouts) = pager.createPipe()
+  if pins != nil:
+    let url = newURL("stream:console").get
+    let replacement = pager.readPipe0("text/plain", CHARSET_UNKNOWN, pins,
+      url, ConsoleTitle, {})
+    replacement.replace = pager.consoleWrapper.container
+    pager.replace(pager.consoleWrapper.container, replacement)
+    pager.consoleWrapper.container = replacement
+    let console = pager.consoleWrapper.console
+    console.err.sclose()
+    console.err = pouts
+
+proc addConsole(pager: Pager; interactive: bool): ConsoleWrapper =
+  if interactive and pager.config.start.console_buffer:
+    let (pins, pouts) = pager.createPipe()
+    if pins != nil:
+      let clearFun = proc() =
+        pager.clearConsole()
+      let showFun = proc() =
+        pager.showConsole()
+      let hideFun = proc() =
+        pager.hideConsole()
+      let url = newURL("stream:console").get
+      let container = pager.readPipe0("text/plain", CHARSET_UNKNOWN, pins,
+        url, ConsoleTitle, {})
+      pouts.write("Type (M-c) console.hide() to return to buffer mode.\n")
+      let console = newConsole(pouts, clearFun, showFun, hideFun)
+      return ConsoleWrapper(console: console, container: container)
+  let err = newPosixStream(STDERR_FILENO)
+  return ConsoleWrapper(console: newConsole(err))
+
+proc flushConsole*(pager: Pager) =
+  if pager.consoleWrapper.console == nil:
+    # hack for when client crashes before console has been initialized
+    pager.consoleWrapper = ConsoleWrapper(
+      console: newConsole(newDynFileStream(stderr))
+    )
+
 proc command(pager: Pager) {.jsfunc.} =
   pager.setLineEdit(lmCommand)
 
@@ -1679,77 +1741,63 @@ proc externFilterSource(pager: Pager; cmd: string; c: Container = nil;
   let fallback = pager.container.contentType.get("text/plain")
   let contentType = contentType.get(fallback)
   let container = pager.newContainerFrom(fromc, contentType)
-  if contentType == "text/html":
-    container.flags.incl(cfIsHTML)
-  else:
-    container.flags.excl(cfIsHTML)
   pager.addContainer(container)
   container.filter = BufferFilter(cmd: cmd)
 
 type CheckMailcapResult = object
-  fdout: int
+  ostream: PosixStream
   ostreamOutputId: int
   connect: bool
   ishtml: bool
   found: bool
+  redirected: bool # whether or not ostream is the same as istream
 
 template myFork(): cint =
   stdout.flushFile()
   stderr.flushFile()
   fork()
 
-# Pipe output of an x-ansioutput mailcap command to the text/x-ansi handler.
-proc ansiDecode(pager: Pager; url: URL; ishtml: var bool; fdin: cint): cint =
-  let entry = pager.config.external.mailcap.getMailcapEntry("text/x-ansi", "",
-    url)
-  var canpipe = true
-  let cmd = unquoteCommand(entry.cmd, "text/x-ansi", "", url, canpipe)
-  if not canpipe:
-    pager.alert("Error: could not pipe to text/x-ansi, decoding as text/plain")
-    return -1
-  var pipefdOutAnsi: array[2, cint]
-  if pipe(pipefdOutAnsi) == -1:
-    pager.alert("Error: failed to open pipe")
-    return
-  case myFork()
+proc execPipe(pager: Pager; cmd: string; ps, os, closeme: PosixStream): int =
+  case (let pid = myFork(); pid)
   of -1:
-    pager.alert("Error: failed to fork ANSI decoder process")
-    discard close(pipefdOutAnsi[0])
-    discard close(pipefdOutAnsi[1])
+    pager.alert("Failed to fork for " & cmd)
+    os.sclose()
     return -1
-  of 0: # child process
-    discard close(pipefdOutAnsi[0])
-    discard dup2(fdin, stdin.getFileHandle())
-    discard close(fdin)
-    discard dup2(pipefdOutAnsi[1], stdout.getFileHandle())
-    discard close(pipefdOutAnsi[1])
+  of 0:
+    discard dup2(ps.fd, STDIN_FILENO)
+    ps.sclose()
+    discard dup2(os.fd, STDOUT_FILENO)
+    os.sclose()
     closeStderr()
+    closeme.sclose()
+    for it in pager.loader.data:
+      if it.stream.fd > 2:
+        it.stream.sclose()
     myExec(cmd)
   else:
-    discard close(pipefdOutAnsi[1])
-    discard close(fdin)
-    ishtml = mfHtmloutput in entry.flags
-    return pipefdOutAnsi[0]
+    os.sclose()
+    return pid
 
-# Pipe input into the mailcap command, then read its output into a buffer.
-# needsterminal is ignored.
-proc runMailcapReadPipe(pager: Pager; stream: SocketStream; cmd: string;
-    pipefdOut: array[2, cint]): int =
-  let pid = myFork()
+# Pipe output of an x-ansioutput mailcap command to the text/x-ansi handler.
+proc ansiDecode(pager: Pager; url: URL; ishtml: var bool; istream: PosixStream):
+    PosixStream =
+  let i = pager.config.external.mailcap.findMailcapEntry("text/x-ansi", "", url)
+  if i == -1:
+    pager.alert("No text/x-ansi entry found")
+    return nil
+  var canpipe = true
+  let cmd = unquoteCommand(pager.config.external.mailcap[i].cmd, "text/x-ansi",
+    "", url, canpipe)
+  if not canpipe:
+    pager.alert("Error: could not pipe to text/x-ansi, decoding as text/plain")
+    return nil
+  let (pins, pouts) = pager.createPipe()
+  if pins == nil:
+    return nil
+  let pid = pager.execPipe(cmd, istream, pouts, pins)
   if pid == -1:
-    pager.alert("Error: failed to fork mailcap read process")
-    return -1
-  elif pid == 0:
-    # child process
-    discard close(pipefdOut[0])
-    discard dup2(stream.fd, stdin.getFileHandle())
-    stream.sclose()
-    discard dup2(pipefdOut[1], stdout.getFileHandle())
-    closeStderr()
-    discard close(pipefdOut[1])
-    myExec(cmd)
-  # parent
-  pid
+    return nil
+  return pins
 
 # Pipe input into the mailcap command, and discard its output.
 # If needsterminal, leave stderr and stdout open and wait for the process.
@@ -1780,7 +1828,7 @@ proc writeToFile(istream: SocketStream; outpath: string): bool =
   let ps = newPosixStream(outpath, O_WRONLY or O_CREAT, 0o600)
   if ps == nil:
     return false
-  var buffer: array[4096, uint8]
+  var buffer {.noinit.}: array[4096, uint8]
   while true:
     let n = istream.recvData(buffer)
     if n == 0:
@@ -1793,23 +1841,27 @@ proc writeToFile(istream: SocketStream; outpath: string): bool =
 # new buffer.
 # needsterminal is ignored.
 proc runMailcapReadFile(pager: Pager; stream: SocketStream;
-    cmd, outpath: string; pipefdOut: array[2, cint]): int =
-  let pid = myFork()
-  if pid == 0:
+    cmd, outpath: string; pins, pouts: PosixStream): int =
+  case (let pid = myFork(); pid)
+  of -1:
+    pager.alert("Error: failed to fork mailcap read process")
+    pouts.sclose()
+    return pid
+  of 0:
     # child process
-    discard close(pipefdOut[0])
-    discard dup2(pipefdOut[1], stdout.getFileHandle())
-    discard close(pipefdOut[1])
+    pins.sclose()
+    discard dup2(pouts.fd, stdout.getFileHandle())
+    pouts.sclose()
     closeStderr()
     if not stream.writeToFile(outpath):
-      #TODO print error message
       quit(1)
     stream.sclose()
     let ret = execCmd(cmd)
     discard tryRemoveFile(outpath)
     quit(ret)
-  # parent
-  pid
+  else: # parent
+    pouts.sclose()
+    return pid
 
 # Save input in a file, run the command, and discard its output.
 # If needsterminal, leave stderr and stdout open and wait for the process.
@@ -1833,7 +1885,6 @@ proc runMailcapWriteFile(pager: Pager; stream: SocketStream;
       closeStdout()
       closeStderr()
       if not stream.writeToFile(outpath):
-        #TODO print error message (maybe in parent?)
         quit(1)
       stream.sclose()
       let ret = execCmd(cmd)
@@ -1842,39 +1893,26 @@ proc runMailcapWriteFile(pager: Pager; stream: SocketStream;
     # parent
     stream.sclose()
 
-proc filterBuffer(pager: Pager; stream: SocketStream; cmd: string;
+proc filterBuffer(pager: Pager; ps: SocketStream; cmd: string;
     ishtml: bool): CheckMailcapResult =
   pager.setEnvVars()
-  var pipefd_out: array[2, cint]
-  if pipe(pipefd_out) == -1:
-    pager.alert("Error: failed to open pipe")
-    return CheckMailcapResult(connect: false, fdout: -1)
-  let pid = myFork()
+  let (pins, pouts) = pager.createPipe()
+  if pins == nil:
+    return CheckMailcapResult(connect: false)
+  let pid = pager.execPipe(cmd, ps, pouts, pins)
   if pid == -1:
-    pager.alert("Error: failed to fork buffer filter process")
-    return CheckMailcapResult(connect: false, fdout: -1)
-  elif pid == 0:
-    # child
-    discard close(pipefd_out[0])
-    discard dup2(stream.fd, stdin.getFileHandle())
-    stream.sclose()
-    discard dup2(pipefd_out[1], stdout.getFileHandle())
-    closeStderr()
-    discard close(pipefd_out[1])
-    myExec(cmd)
-  # parent
-  discard close(pipefd_out[1])
-  let fdout = pipefd_out[0]
+    return CheckMailcapResult(connect: false)
   let url = parseURL("stream:" & $pid).get
-  pager.loader.passFd(url.pathname, FileHandle(fdout))
-  safeClose(fdout)
+  pager.loader.passFd(url.pathname, FileHandle(pins.fd))
+  pins.safeClose()
   let response = pager.loader.doRequest(newRequest(url))
   return CheckMailcapResult(
     connect: true,
-    fdout: response.body.fd,
+    ostream: response.body,
     ostreamOutputId: response.outputId,
     ishtml: ishtml,
-    found: true
+    found: true,
+    redirected: true
   )
 
 # Search for a mailcap entry, and if found, execute the specified command
@@ -1887,42 +1925,13 @@ proc filterBuffer(pager: Pager; stream: SocketStream; cmd: string;
 # If needsterminal is specified, and stdout is not being read, then the
 # pager is suspended until the command exits.
 #TODO add support for edit/compose, better error handling
-proc checkMailcap(pager: Pager; container: Container; stream: SocketStream;
-    istreamOutputId: int; contentType: string): CheckMailcapResult =
-  if container.filter != nil:
-    return pager.filterBuffer(
-      stream,
-      container.filter.cmd,
-      cfIsHTML in container.flags
-    )
-  # contentType must exist, because we set it in applyResponse
-  let shortContentType = container.contentType.get
-  if shortContentType == "text/html":
-    # We support text/html natively, so it would make little sense to execute
-    # mailcap filters for it.
-    return CheckMailcapResult(
-      connect: true,
-      fdout: stream.fd,
-      ishtml: true,
-      found: true
-    )
-  if shortContentType == "text/plain":
-    # text/plain could potentially be useful. Unfortunately, many mailcaps
-    # include a text/plain entry with less by default, so it's probably better
-    # to ignore this.
-    return CheckMailcapResult(connect: true, fdout: stream.fd, found: true)
-  #TODO callback for outpath or something
-  let url = container.url
-  let entry = pager.config.external.mailcap.getMailcapEntry(contentType, "",
-    url)
-  if entry == nil:
-    return CheckMailcapResult(connect: true, fdout: stream.fd, found: false)
+proc checkMailcap0(pager: Pager; url: URL; stream: SocketStream;
+    istreamOutputId: int; contentType: string; entry: MailcapEntry):
+    CheckMailcapResult =
   let ext = url.pathname.afterLast('.')
-  let tempfile = pager.getTempFile(ext)
-  let outpath = if entry.nametemplate != "":
-    unquoteCommand(entry.nametemplate, contentType, tempfile, url)
-  else:
-    tempfile
+  var outpath = pager.getTempFile(ext)
+  if entry.nametemplate != "":
+    outpath = unquoteCommand(entry.nametemplate, contentType, outpath, url)
   var canpipe = true
   let cmd = unquoteCommand(entry.cmd, contentType, outpath, url, canpipe)
   var ishtml = mfHtmloutput in entry.flags
@@ -1938,34 +1947,67 @@ proc checkMailcap(pager: Pager; container: Container; stream: SocketStream;
         pager.runMailcapWriteFile(stream, needsterminal, cmd, outpath)
       # stream is already closed
       break needsConnect # never connect here, since there's no output
-    var pipefdOut: array[2, cint]
-    if pipe(pipefdOut) == -1:
-      pager.alert("Error: failed to open pipe")
+    var (pins, pouts) = pager.createPipe()
+    if pins == nil:
       stream.sclose() # connect: false implies that we consumed the stream
       break needsConnect
     let pid = if canpipe:
-      pager.runMailcapReadPipe(stream, cmd, pipefdOut)
-    else:
-      pager.runMailcapReadFile(stream, cmd, outpath, pipefdOut)
-    discard close(pipefdOut[1]) # close write
-    let fdout = if not ishtml and mfAnsioutput in entry.flags:
-      pager.ansiDecode(url, ishtml, pipefdOut[0])
+      # Pipe input into the mailcap command, then read its output into a buffer.
+      # needsterminal is ignored.
+      pager.execPipe(cmd, stream, pouts, pins)
     else:
-      pipefdOut[0]
+      pager.runMailcapReadFile(stream, cmd, outpath, pins, pouts)
+    stream.sclose()
+    if pid == -1:
+      break needsConnect
+    if not ishtml and mfAnsioutput in entry.flags:
+      pins = pager.ansiDecode(url, ishtml, pins)
     delEnv("MAILCAP_URL")
     let url = parseURL("stream:" & $pid).get
-    pager.loader.passFd(url.pathname, FileHandle(fdout))
-    safeClose(cint(fdout))
+    pager.loader.passFd(url.pathname, FileHandle(pins.fd))
+    pins.safeClose()
     let response = pager.loader.doRequest(newRequest(url))
     return CheckMailcapResult(
       connect: true,
-      fdout: response.body.fd,
+      ostream: response.body,
       ostreamOutputId: response.outputId,
       ishtml: ishtml,
-      found: true
+      found: true,
+      redirected: true
     )
   delEnv("MAILCAP_URL")
-  return CheckMailcapResult(connect: false, fdout: -1, found: true)
+  return CheckMailcapResult(connect: false, found: true)
+
+proc checkMailcap(pager: Pager; container: Container; stream: SocketStream;
+    istreamOutputId: int; contentType: string): CheckMailcapResult =
+  # contentType must exist, because we set it in applyResponse
+  let shortContentType = container.contentType.get
+  if container.filter != nil:
+    return pager.filterBuffer(
+      stream,
+      container.filter.cmd,
+      shortContentType.equalsIgnoreCase("text/html")
+    )
+  if shortContentType.equalsIgnoreCase("text/html"):
+    # We support text/html natively, so it would make little sense to execute
+    # mailcap filters for it.
+    return CheckMailcapResult(
+      connect: true,
+      ostream: stream,
+      ishtml: true,
+      found: true
+    )
+  if shortContentType.equalsIgnoreCase("text/plain"):
+    # text/plain could potentially be useful. Unfortunately, many mailcaps
+    # include a text/plain entry with less by default, so it's probably better
+    # to ignore this.
+    return CheckMailcapResult(connect: true, ostream: stream, found: true)
+  let url = container.url
+  let i = pager.config.external.mailcap.findMailcapEntry(contentType, "", url)
+  if i == -1:
+    return CheckMailcapResult(connect: true, ostream: stream, found: false)
+  return pager.checkMailcap0(url, stream, istreamOutputId, contentType,
+    pager.config.external.mailcap[i])
 
 proc redirectTo(pager: Pager; container: Container; request: Request) =
   let replaceBackup = if container.replaceBackup != nil:
@@ -2076,13 +2118,10 @@ proc connected(pager: Pager; container: Container; response: Response) =
       mailcapRes.ishtml,
       container.charsetStack
     )
-    if mailcapRes.fdout != istream.fd:
-      # istream has been redirected into a filter
-      istream.sclose()
     pager.procmap.add(ProcMapItem(
       container: container,
-      fdout: FileHandle(mailcapRes.fdout),
-      fdin: FileHandle(istream.fd),
+      ostream: mailcapRes.ostream,
+      redirected: mailcapRes.redirected,
       ostreamOutputId: mailcapRes.ostreamOutputId,
       istreamOutputId: response.outputId
     ))
diff --git a/src/server/forkserver.nim b/src/server/forkserver.nim
index 06c930f6..0190a425 100644
--- a/src/server/forkserver.nim
+++ b/src/server/forkserver.nim
@@ -10,7 +10,6 @@ import io/bufreader
 import io/bufwriter
 import io/dynstream
 import io/serversocket
-import io/stdio
 import loader/loader
 import loader/loaderiface
 import server/buffer