about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/html/env.nim33
-rw-r--r--src/io/bufreader.nim1
-rw-r--r--src/io/bufwriter.nim1
-rw-r--r--src/io/dynstream.nim9
-rw-r--r--src/io/poll.nim40
-rw-r--r--src/js/timeout.nim113
-rw-r--r--src/loader/loader.nim63
-rw-r--r--src/local/client.nim130
-rw-r--r--src/local/pager.nim11
-rw-r--r--src/server/buffer.nim161
-rw-r--r--src/server/forkserver.nim7
-rw-r--r--src/utils/sandbox.nim13
12 files changed, 299 insertions, 283 deletions
diff --git a/src/html/env.nim b/src/html/env.nim
index ab9a9be5..27a12816 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -1,4 +1,3 @@
-import std/selectors
 import std/tables
 
 import html/catom
@@ -265,25 +264,22 @@ proc addWindowModule2*(ctx: JSContext) =
   ctx.registerType(Window, parent = eventTargetCID, asglobal = true,
     globalparent = true)
 
-proc addScripting*(window: Window; selector: Selector[int]) =
+proc evalJSFree(opaque: RootRef; src, file: string) =
+  let window = Window(opaque)
+  let ret = window.jsctx.eval(src, file, JS_EVAL_TYPE_GLOBAL)
+  if JS_IsException(ret):
+    window.console.log("Exception in document", $window.document.url,
+      window.jsctx.getExceptionMsg())
+  else:
+    JS_FreeValue(window.jsctx, ret)
+
+proc addScripting*(window: Window) =
   let rt = newJSRuntime()
   let ctx = rt.newJSContext()
   window.jsrt = rt
   window.jsctx = ctx
   window.importMapsAllowed = true
-  window.timeouts = newTimeoutState(
-    selector = selector,
-    jsctx = ctx,
-    err = window.console.err,
-    evalJSFree = (proc(src, file: string) =
-      let ret = window.jsctx.eval(src, file, JS_EVAL_TYPE_GLOBAL)
-      if JS_IsException(ret):
-        window.console.log("Exception in document", $window.document.url,
-          window.jsctx.getExceptionMsg())
-      else:
-        JS_FreeValue(ctx, ret)
-    )
-  )
+  window.timeouts = newTimeoutState(ctx, window.console.err, evalJSFree, window)
   ctx.addWindowModule()
   ctx.setGlobal(window)
   ctx.addDOMExceptionModule()
@@ -309,9 +305,8 @@ proc runJSJobs*(window: Window) =
     let ctx = r.error
     ctx.writeException(window.console.err)
 
-proc newWindow*(scripting, images, styling: bool; selector: Selector[int];
-    attrs: WindowAttributes; factory: CAtomFactory; loader: FileLoader;
-    url: URL): Window =
+proc newWindow*(scripting, images, styling: bool; attrs: WindowAttributes;
+    factory: CAtomFactory; loader: FileLoader; url: URL): Window =
   let err = newDynFileStream(stderr)
   let window = Window(
     attrs: attrs,
@@ -328,5 +323,5 @@ proc newWindow*(scripting, images, styling: bool; selector: Selector[int];
   )
   window.location = window.newLocation()
   if scripting:
-    window.addScripting(selector)
+    window.addScripting()
   return window
diff --git a/src/io/bufreader.nim b/src/io/bufreader.nim
index 46f8e0f3..718e6d55 100644
--- a/src/io/bufreader.nim
+++ b/src/io/bufreader.nim
@@ -1,5 +1,4 @@
 import std/options
-import std/sets
 import std/tables
 
 import io/dynstream
diff --git a/src/io/bufwriter.nim b/src/io/bufwriter.nim
index 77b8ebd8..3eea01fa 100644
--- a/src/io/bufwriter.nim
+++ b/src/io/bufwriter.nim
@@ -2,7 +2,6 @@
 # Each packet is prefixed with its length as a pointer-sized integer.
 
 import std/options
-import std/sets
 import std/tables
 
 import io/dynstream
diff --git a/src/io/dynstream.nim b/src/io/dynstream.nim
index 66a2a932..b0b07140 100644
--- a/src/io/dynstream.nim
+++ b/src/io/dynstream.nim
@@ -251,6 +251,15 @@ proc dealloc*(mem: MaybeMappedMemory) =
   else:
     dealloc(mem.p0)
 
+proc drain*(ps: PosixStream) =
+  assert not ps.blocking
+  var buffer {.noinit.}: array[4096, uint8]
+  try:
+    while true:
+      discard ps.recvData(addr buffer[0], buffer.len)
+  except ErrorAgain:
+    discard
+
 type SocketStream* = ref object of PosixStream
   source*: Socket
 
diff --git a/src/io/poll.nim b/src/io/poll.nim
new file mode 100644
index 00000000..3c2c29a8
--- /dev/null
+++ b/src/io/poll.nim
@@ -0,0 +1,40 @@
+import std/posix
+
+type PollData* = object
+  fds: seq[TPollFd]
+
+iterator events*(ctx: PollData): TPollFd =
+  let L = ctx.fds.len
+  for i in 0 ..< L:
+    let event = ctx.fds[i]
+    if event.fd == -1 or ctx.fds[i].revents == 0:
+      continue
+    assert (event.revents and POLLNVAL) == 0
+    yield event
+
+proc register*(ctx: var PollData; fd: int; events: cshort) =
+  if fd >= ctx.fds.len:
+    ctx.fds.setLen(fd + 1)
+  ctx.fds[fd].fd = cint(fd)
+  ctx.fds[fd].events = events
+
+proc register*(ctx: var PollData; fd: cint; events: cshort) =
+  ctx.register(int(fd), events)
+
+proc unregister*(ctx: var PollData; fd: int) =
+  ctx.fds[fd].fd = -1
+
+proc trim(ctx: var PollData) =
+  var i = ctx.fds.high
+  while i >= 0:
+    if ctx.fds[i].fd != -1:
+      break
+    dec i
+  ctx.fds.setLen(i + 1)
+
+proc clear*(ctx: var PollData) =
+  ctx.fds.setLen(0)
+
+proc poll*(ctx: var PollData; timeout: cint) =
+  ctx.trim()
+  discard poll(addr ctx.fds[0], Tnfds(ctx.fds.len), cint(timeout))
diff --git a/src/js/timeout.nim b/src/js/timeout.nim
index 0213156a..72a68dbc 100644
--- a/src/js/timeout.nim
+++ b/src/js/timeout.nim
@@ -1,5 +1,5 @@
-import std/selectors
-import std/tables
+import std/algorithm
+import std/times
 
 import io/dynstream
 import js/console
@@ -15,59 +15,76 @@ type
 
   TimeoutEntry = ref object
     t: TimeoutType
-    fd: int
+    id: int32
     val: JSValue
     args: seq[JSValue]
+    expires: int64
+    timeout: int32
+
+  EvalJSFree* = proc(opaque: RootRef; src, file: string) {.nimcall.}
 
   TimeoutState* = ref object
     timeoutid: int32
-    timeouts: Table[int32, TimeoutEntry]
-    timeoutFds: Table[int, int32]
-    selector: Selector[int] #TODO would be better with void...
+    timeouts: seq[TimeoutEntry]
     jsctx: JSContext
     err: DynStream #TODO shouldn't be needed
-    evalJSFree: proc(src, file: string) #TODO ew
+    evalJSFree: EvalJSFree
+    opaque: RootRef
+    sorted: bool
 
-func newTimeoutState*(selector: Selector[int]; jsctx: JSContext; err: DynStream;
-    evalJSFree: proc(src, file: string)): TimeoutState =
+func newTimeoutState*(jsctx: JSContext; err: DynStream;
+    evalJSFree: EvalJSFree; opaque: RootRef): TimeoutState =
   return TimeoutState(
-    selector: selector,
     jsctx: jsctx,
     err: err,
-    evalJSFree: evalJSFree
+    evalJSFree: evalJSFree,
+    opaque: opaque,
+    sorted: true
   )
 
 func empty*(state: TimeoutState): bool =
   return state.timeouts.len == 0
 
+proc clearTimeout0(state: var TimeoutState; i: int) =
+  let entry = state.timeouts[i]
+  JS_FreeValue(state.jsctx, entry.val)
+  for arg in entry.args:
+    JS_FreeValue(state.jsctx, arg)
+  state.timeouts.del(i)
+  if state.timeouts.len != i: # only set if we del'd in the middle
+    state.sorted = false
+
 proc clearTimeout*(state: var TimeoutState; id: int32) =
-  if id in state.timeouts:
-    let entry = state.timeouts[id]
-    state.selector.unregister(entry.fd)
-    JS_FreeValue(state.jsctx, entry.val)
-    for arg in entry.args:
-      JS_FreeValue(state.jsctx, arg)
-    state.timeoutFds.del(entry.fd)
-    state.timeouts.del(id)
+  var j = -1
+  for i in 0 ..< state.timeouts.len:
+    if state.timeouts[i].id == id:
+      j = i
+      break
+  if j != -1:
+    state.clearTimeout0(j)
+
+proc getUnixMillis(): int64 =
+  let now = getTime()
+  return now.toUnix() * 1000 + now.nanosecond div 1_000_000
 
-#TODO varargs
 proc setTimeout*(state: var TimeoutState; t: TimeoutType; handler: JSValue;
     timeout: int32; args: openArray[JSValue]): int32 =
   let id = state.timeoutid
   inc state.timeoutid
-  let fd = state.selector.registerTimer(max(timeout, 1), t == ttTimeout, 0)
-  state.timeoutFds[fd] = id
   let entry = TimeoutEntry(
     t: t,
-    fd: fd,
-    val: JS_DupValue(state.jsctx, handler)
+    id: id,
+    val: JS_DupValue(state.jsctx, handler),
+    expires: getUnixMillis() + int64(timeout),
+    timeout: timeout
   )
   for arg in args:
     entry.args.add(JS_DupValue(state.jsctx, arg))
-  state.timeouts[id] = entry
+  state.timeouts.add(entry)
+  state.sorted = false
   return id
 
-proc runEntry(state: var TimeoutState; entry: TimeoutEntry; name: string) =
+proc runEntry(state: var TimeoutState; entry: TimeoutEntry) =
   if JS_IsFunction(state.jsctx, entry.val):
     let ret = JS_Call(state.jsctx, entry.val, JS_UNDEFINED,
       cint(entry.args.len), entry.args.toJSValueArray())
@@ -77,23 +94,39 @@ proc runEntry(state: var TimeoutState; entry: TimeoutEntry; name: string) =
   else:
     var s: string
     if state.jsctx.fromJS(entry.val, s).isSome:
-      state.evalJSFree(s, name)
+      state.evalJSFree(state.opaque, s, $entry.t)
+
+# for poll
+proc sortAndGetTimeout*(state: var TimeoutState): cint =
+  if state.timeouts.len == 0:
+    return -1
+  if not state.sorted:
+    state.timeouts.sort(proc(a, b: TimeoutEntry): int =
+      cmp(a.expires, b.expires), order = Descending)
+    state.sorted = true
+  let now = getUnixMillis()
+  return cint(max(state.timeouts[^1].expires - now, -1))
 
-proc runTimeoutFd*(state: var TimeoutState; fd: int): bool =
-  if fd notin state.timeoutFds:
-    return false
-  let id = state.timeoutFds[fd]
-  let entry = state.timeouts[id]
-  state.runEntry(entry, $entry.t)
-  if entry.t == ttTimeout:
-    state.clearTimeout(id)
-  return true
+proc run*(state: var TimeoutState): bool =
+  let H = state.timeouts.high
+  let now = getUnixMillis()
+  var found = false
+  for i in countdown(H, 0):
+    if state.timeouts[i].expires > now:
+      break
+    let entry = state.timeouts[i]
+    state.runEntry(entry)
+    found = true
+    case entry.t
+    of ttTimeout: state.clearTimeout0(i)
+    of ttInterval:
+      entry.expires = now + entry.timeout
+      state.sorted = false
+  return found
 
 proc clearAll*(state: var TimeoutState) =
-  for entry in state.timeouts.values:
-    state.selector.unregister(entry.fd)
+  for entry in state.timeouts:
     JS_FreeValue(state.jsctx, entry.val)
     for arg in entry.args:
       JS_FreeValue(state.jsctx, arg)
-  state.timeouts.clear()
-  state.timeoutFds.clear()
+  state.timeouts.setLen(0)
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index 85490b84..e8d29ee2 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -26,13 +26,13 @@ import std/net
 import std/options
 import std/os
 import std/posix
-import std/selectors
 import std/strutils
 import std/tables
 
 import io/bufreader
 import io/bufwriter
 import io/dynstream
+import io/poll
 import io/serversocket
 import io/stdio
 import io/tempfile
@@ -42,7 +42,6 @@ import loader/headers
 import loader/loaderhandle
 import loader/loaderiface
 import loader/request
-import loader/response
 import monoucha/javascript
 import types/cookie
 import types/formdata
@@ -52,9 +51,6 @@ import types/urimethodmap
 import types/url
 import utils/twtstr
 
-export request
-export response
-
 type
   CachedItem = ref object
     id: int
@@ -77,7 +73,7 @@ type
     alive: bool
     config: LoaderConfig
     handleMap: seq[LoaderHandle]
-    selector: Selector[int]
+    pollData: PollData
     # List of existing clients (buffer or pager) that may make requests.
     clientData: Table[int, ClientData] # pid -> data
     # ID of next output. TODO: find a better allocation scheme
@@ -145,40 +141,22 @@ type PushBufferResult = enum
 
 proc register(ctx: LoaderContext; handle: InputHandle) =
   assert not handle.registered
-  ctx.selector.registerHandle(int(handle.stream.fd), {Read}, 0)
+  ctx.pollData.register(handle.stream.fd, cshort(POLLIN))
   handle.registered = true
 
 proc unregister(ctx: LoaderContext; handle: InputHandle) =
   assert handle.registered
-  ctx.selector.unregister(int(handle.stream.fd))
+  ctx.pollData.unregister(int(handle.stream.fd))
   handle.registered = false
 
 proc register(ctx: LoaderContext; output: OutputHandle) =
   assert not output.registered
-  ctx.selector.registerHandle(int(output.stream.fd), {Write}, 0)
+  ctx.pollData.register(int(output.stream.fd), cshort(POLLOUT))
   output.registered = true
 
-const bsdPlatform = defined(macosx) or defined(freebsd) or defined(netbsd) or
-  defined(openbsd) or defined(dragonfly)
 proc unregister(ctx: LoaderContext; output: OutputHandle) =
   assert output.registered
-  # so kqueue-based selectors raise when we try to unregister a pipe whose
-  # reader is at EOF. "solution": clean up this mess ourselves.
-  let fd = int(output.stream.fd)
-  when bsdPlatform:
-    let oc = ctx.selector.count
-    try:
-      ctx.selector.unregister(fd)
-    except IOSelectorsException:
-      # ????
-      for name, f in ctx.selector[].fieldPairs:
-        when name == "fds":
-          cast[ptr int](addr f[fd])[] = -1
-        elif name == "changes":
-          f.setLen(0)
-      ctx.selector.count = oc - 1
-  else:
-    ctx.selector.unregister(fd)
+  ctx.pollData.unregister(int(output.stream.fd))
   output.registered = false
 
 # Either write data to the target output, or append it to the list of buffers to
@@ -1178,17 +1156,13 @@ proc exitLoader(ctx: LoaderContext) =
 
 var gctx: LoaderContext
 proc initLoaderContext(fd: cint; config: LoaderConfig): LoaderContext =
-  var ctx = LoaderContext(
-    alive: true,
-    config: config,
-    selector: newSelector[int]()
-  )
+  var ctx = LoaderContext(alive: true, config: config)
   gctx = ctx
   let myPid = getCurrentProcessId()
   # we don't capsicumize loader, so -1 is appropriate here
   ctx.ssock = initServerSocket(config.sockdir, -1, myPid, blocking = true)
   let sfd = int(ctx.ssock.sock.getFd())
-  ctx.selector.registerHandle(sfd, {Read}, 0)
+  ctx.pollData.register(sfd, POLLIN)
   if sfd >= ctx.handleMap.len:
     ctx.handleMap.setLen(sfd + 1)
   ctx.handleMap[sfd] = LoaderHandle() # pseudo handle
@@ -1302,25 +1276,26 @@ proc finishCycle(ctx: LoaderContext; unregRead: var seq[InputHandle];
 proc runFileLoader*(fd: cint; config: LoaderConfig) =
   var ctx = initLoaderContext(fd, config)
   let fd = int(ctx.ssock.sock.getFd())
-  var keys: array[64, ReadyKey]
   while ctx.alive:
-    let count = ctx.selector.selectInto(-1, keys)
+    ctx.pollData.poll(-1)
     var unregRead: seq[InputHandle] = @[]
     var unregWrite: seq[OutputHandle] = @[]
-    for event in keys.toOpenArray(0, count - 1):
-      let handle = ctx.handleMap[event.fd]
-      if Read in event.events:
-        if event.fd == fd: # incoming connection
+    for event in ctx.pollData.events:
+      let efd = int(event.fd)
+      if (event.revents and POLLIN) != 0:
+        if efd == fd: # incoming connection
           ctx.acceptConnection()
         else:
-          let handle = InputHandle(ctx.handleMap[event.fd])
+          let handle = InputHandle(ctx.handleMap[efd])
           case ctx.handleRead(handle, unregWrite)
           of hrrDone: discard
           of hrrUnregister, hrrBrokenPipe: unregRead.add(handle)
-      if Write in event.events:
+      if (event.revents and POLLOUT) != 0:
+        let handle = ctx.handleMap[efd]
         ctx.handleWrite(OutputHandle(handle), unregWrite)
-      if Error in event.events:
-        assert event.fd != fd
+      if (event.revents and POLLERR) != 0 or (event.revents and POLLHUP) != 0:
+        assert efd != fd
+        let handle = ctx.handleMap[efd]
         if handle of InputHandle: # istream died
           unregRead.add(InputHandle(handle))
         else: # ostream died
diff --git a/src/local/client.nim b/src/local/client.nim
index 6c1d505c..f32e758d 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -4,7 +4,6 @@ import std/net
 import std/options
 import std/os
 import std/posix
-import std/selectors
 import std/strutils
 import std/tables
 
@@ -19,6 +18,7 @@ import html/formdata
 import html/xmlhttprequest
 import io/bufwriter
 import io/dynstream
+import io/poll
 import io/promise
 import io/serversocket
 import js/console
@@ -74,8 +74,8 @@ type
 func console(client: Client): Console {.jsfget.} =
   return client.consoleWrapper.console
 
-template selector(client: Client): Selector[int] =
-  client.pager.selector
+template pollData(client: Client): PollData =
+  client.pager.pollData
 
 template forkserver(client: Client): ForkServer =
   client.pager.forkserver
@@ -144,6 +144,10 @@ proc evalJS(client: Client; src, filename: string; module = false): JSValue =
 proc evalJSFree(client: Client; src, filename: string) =
   JS_FreeValue(client.jsctx, client.evalJS(src, filename))
 
+proc evalJSFree2(opaque: RootRef; src, filename: string) =
+  let client = Client(opaque)
+  client.evalJSFree(src, filename)
+
 proc command0(client: Client; src: string; filename = "<command>";
     silence = false; module = false) =
   let ret = client.evalJS(src, filename, module = module)
@@ -358,9 +362,6 @@ proc input(client: Client): EmptyPromise =
     p.resolve()
   return p
 
-when ioselSupportedPlatform:
-  let SIGWINCH {.importc, header: "<signal.h>", nodecl.}: cint
-
 proc showConsole(client: Client) {.jsfunc.} =
   let container = client.consoleWrapper.container
   if client.pager.container != container:
@@ -381,7 +382,7 @@ proc acceptBuffers(client: Client) =
     if container.iface != nil: # fully connected
       let stream = container.iface.stream
       let fd = int(stream.source.fd)
-      client.selector.unregister(fd)
+      client.pollData.unregister(fd)
       client.loader.unset(fd)
       stream.sclose()
     elif container.process != -1: # connecting to buffer process
@@ -390,12 +391,12 @@ proc acceptBuffers(client: Client) =
     elif (let item = pager.findConnectingContainer(container); item != nil):
       # connecting to URL
       let stream = item.stream
-      client.selector.unregister(int(stream.fd))
+      client.pollData.unregister(int(stream.fd))
       stream.sclose()
       client.loader.unset(item)
   let registerFun = proc(fd: int) =
-    client.selector.unregister(fd)
-    client.selector.registerHandle(fd, {Read, Write}, 0)
+    client.pollData.unregister(fd)
+    client.pollData.register(fd, POLLIN or POLLOUT)
   for item in pager.procmap:
     let container = item.container
     let stream = connectSocketStream(client.config.external.sockdir,
@@ -444,7 +445,7 @@ proc acceptBuffers(client: Client) =
       container.setCloneStream(stream, registerFun)
     let fd = int(stream.fd)
     client.loader.put(ContainerData(stream: stream, container: container))
-    client.selector.registerHandle(fd, {Read}, 0)
+    client.pollData.register(fd, POLLIN)
     pager.handleEvents(container)
   pager.procmap.setLen(0)
 
@@ -505,8 +506,8 @@ proc handleRead(client: Client; fd: int) =
 proc handleWrite(client: Client; fd: int) =
   let container = ContainerData(client.loader.get(fd)).container
   if container.iface.stream.flushWrite():
-    client.selector.unregister(fd)
-    client.selector.registerHandle(fd, {Read}, 0)
+    client.pollData.unregister(fd)
+    client.pollData.register(fd, POLLIN)
 
 proc flushConsole*(client: Client) {.jsfunc.} =
   if client.console == nil:
@@ -534,7 +535,7 @@ proc handleError(client: Client; fd: int) =
         client.console.error("Error in buffer", $container.url)
       else:
         client.consoleWrapper.container = nil
-      client.selector.unregister(fd)
+      client.pollData.unregister(fd)
       client.loader.unset(fd)
       doAssert client.consoleWrapper.container != nil
       client.showConsole()
@@ -546,31 +547,48 @@ proc handleError(client: Client; fd: int) =
     doAssert client.consoleWrapper.container != nil
     client.showConsole()
 
+let SIGWINCH {.importc, header: "<signal.h>", nodecl.}: cint
+
+proc setupSigwinch(client: Client): PosixStream =
+  var pipefd {.noinit.}: array[2, cint]
+  doAssert pipe(pipefd) != -1
+  let writer = newPosixStream(pipefd[1])
+  writer.setBlocking(false)
+  var gwriter {.global.}: PosixStream = nil
+  gwriter = writer
+  onSignal SIGWINCH:
+    discard sig
+    try:
+      gwriter.sendDataLoop([0u8])
+    except ErrorAgain:
+      discard
+  let reader = newPosixStream(pipefd[0])
+  reader.setBlocking(false)
+  return reader
+
 proc inputLoop(client: Client) =
-  let selector = client.selector
-  selector.registerHandle(int(client.pager.term.istream.fd), {Read}, 0)
-  when ioselSupportedPlatform:
-    let sigwinch = selector.registerSignal(int(SIGWINCH), 0)
-  var keys: array[64, ReadyKey]
+  client.pollData.register(client.pager.term.istream.fd, POLLIN)
+  let sigwinch = client.setupSigwinch()
+  client.pollData.register(sigwinch.fd, POLLIN)
   while true:
-    let count = client.selector.selectInto(-1, keys)
-    for event in keys.toOpenArray(0, count - 1):
-      if Read in event.events:
-        client.handleRead(event.fd)
-      if Write in event.events:
-        client.handleWrite(event.fd)
-      if Error in event.events:
-        client.handleError(event.fd)
-      when ioselSupportedPlatform:
-        if Signal in event.events:
-          assert event.fd == sigwinch
+    let timeout = client.timeouts.sortAndGetTimeout()
+    client.pollData.poll(timeout)
+    for event in client.pollData.events:
+      let efd = int(event.fd)
+      if (event.revents and POLLIN) != 0:
+        if event.fd == sigwinch.fd:
+          sigwinch.drain()
           client.pager.windowChange()
-      if selectors.Event.Timer in event.events:
-        let r = client.timeouts.runTimeoutFd(event.fd)
-        assert r
-        let container = client.consoleWrapper.container
-        if container != nil:
-          container.tailOnLoad = true
+        else:
+          client.handleRead(efd)
+      if (event.revents and POLLOUT) != 0:
+        client.handleWrite(efd)
+      if (event.revents and POLLERR) != 0 or (event.revents and POLLHUP) != 0:
+        client.handleError(efd)
+    if client.timeouts.run():
+      let container = client.consoleWrapper.container
+      if container != nil:
+        container.tailOnLoad = true
     client.runJSJobs()
     client.loader.unregistered.setLen(0)
     client.acceptBuffers()
@@ -604,19 +622,18 @@ func hasSelectFds(client: Client): bool =
     client.pager.procmap.len > 0
 
 proc headlessLoop(client: Client) =
-  var keys: array[64, ReadyKey]
   while client.hasSelectFds():
-    let count = client.selector.selectInto(-1, keys)
-    for event in keys.toOpenArray(0, count - 1):
-      if Read in event.events:
-        client.handleRead(event.fd)
-      if Write in event.events:
-        client.handleWrite(event.fd)
-      if Error in event.events:
-        client.handleError(event.fd)
-      if selectors.Event.Timer in event.events:
-        let r = client.timeouts.runTimeoutFd(event.fd)
-        assert r
+    let timeout = client.timeouts.sortAndGetTimeout()
+    client.pollData.poll(timeout)
+    for event in client.pollData.events:
+      let efd = int(event.fd)
+      if (event.revents and POLLIN) != 0:
+        client.handleRead(efd)
+      if (event.revents and POLLOUT) != 0:
+        client.handleWrite(efd)
+      if (event.revents and POLLERR) != 0 or (event.revents and POLLHUP) != 0:
+        client.handleError(efd)
+    discard client.timeouts.run()
     client.runJSJobs()
     client.loader.unregistered.setLen(0)
     client.acceptBuffers()
@@ -721,26 +738,25 @@ proc launchClient*(client: Client; pages: seq[string];
         dump = false
     else:
       dump = true
-  let selector = newSelector[int]()
-  let efd = int(client.forkserver.estream.fd)
-  selector.registerHandle(efd, {Read}, 0)
+  let pager = client.pager
+  pager.pollData.register(client.forkserver.estream.fd, POLLIN)
   client.loader.registerFun = proc(fd: int) =
-    selector.registerHandle(fd, {Read}, 0)
+    pager.pollData.register(fd, POLLIN)
   client.loader.unregisterFun = proc(fd: int) =
-    selector.unregister(fd)
-  client.pager.launchPager(istream, selector)
+    pager.pollData.unregister(fd)
+  pager.launchPager(istream)
   let clearFun = proc() =
     client.clearConsole()
   let showFun = proc() =
     client.showConsole()
   let hideFun = proc() =
     client.hideConsole()
-  client.consoleWrapper = client.pager.addConsole(interactive = istream != nil,
+  client.consoleWrapper = pager.addConsole(interactive = istream != nil,
     clearFun, showFun, hideFun)
   #TODO passing console.err here makes it impossible to change it later. maybe
   # better associate it with jsctx
-  client.timeouts = newTimeoutState(client.selector, client.jsctx,
-    client.console.err, proc(src, file: string) = client.evalJSFree(src, file))
+  client.timeouts = newTimeoutState(client.jsctx, client.console.err,
+    evalJSFree2, client)
   client.pager.timeouts = client.timeouts
   addExitProc((proc() = client.cleanup()))
   if client.config.start.startup_script != "":
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 35f6ab4c..58ab9d77 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -4,7 +4,6 @@ import std/options
 import std/os
 import std/osproc
 import std/posix
-import std/selectors
 import std/sets
 import std/tables
 
@@ -14,6 +13,7 @@ import config/config
 import config/mailcap
 import io/bufreader
 import io/dynstream
+import io/poll
 import io/promise
 import io/stdio
 import io/tempfile
@@ -135,21 +135,21 @@ type
     jsctx: JSContext
     lastAlert: string # last alert seen by the user
     lineData: LineData
-    lineedit*: LineEdit
     lineHist: array[LineMode, LineHistory]
+    lineedit*: LineEdit
     linemode: LineMode
     loader*: FileLoader
     luctx: LUContext
     navDirection {.jsget.}: NavDirection
     notnum*: bool # has a non-numeric character been input already?
     numload*: int # number of pages currently being loaded
+    pollData*: PollData
     precnum*: int32 # current number prefix (when vi-numeric-prefix is true)
     procmap*: seq[ProcMapItem]
     refreshAllowed: HashSet[string]
     regex: Opt[Regex]
     reverseSearch: bool
     scommand*: string
-    selector*: Selector[int]
     status: Surface
     term*: Terminal
     timeouts*: TimeoutState
@@ -365,8 +365,7 @@ proc setLoader*(pager: Pager; loader: FileLoader) =
   )
   loader.key = pager.addLoaderClient(pager.loader.clientPid, config)
 
-proc launchPager*(pager: Pager; istream: PosixStream; selector: Selector[int]) =
-  pager.selector = selector
+proc launchPager*(pager: Pager; istream: PosixStream) =
   case pager.term.start(istream)
   of tsrSuccess: discard
   of tsrDA1Fail:
@@ -2045,7 +2044,7 @@ proc connected(pager: Pager; container: Container; response: Response) =
     pager.refreshStatusMsg()
 
 proc unregisterFd(pager: Pager; fd: int) =
-  pager.selector.unregister(fd)
+  pager.pollData.unregister(fd)
   pager.loader.unregistered.add(fd)
 
 proc handleRead*(pager: Pager; item: ConnectingContainer) =
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index af07f3f9..8adc3fd5 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -6,7 +6,6 @@ import std/net
 import std/options
 import std/os
 import std/posix
-import std/selectors
 import std/tables
 
 import chagashi/charset
@@ -29,6 +28,7 @@ import html/formdata as formdata_impl
 import io/bufreader
 import io/bufwriter
 import io/dynstream
+import io/poll
 import io/promise
 import io/serversocket
 import js/console
@@ -76,45 +76,44 @@ type
     y*: int
     str*: string
 
-  Buffer* = ref object
-    rfd: int # file descriptor of command pipe
+  Buffer = ref object
+    attrs: WindowAttributes
+    bgcolor: CellColor
+    bytesRead: int
+    cacheId: int
+    charset: Charset
+    charsetStack: seq[Charset]
+    config: BufferConfig
+    ctx: TextDecoderContext
+    document: Document
+    estream: DynFileStream # error stream
+    factory: CAtomFactory
     fd: int # file descriptor of buffer source
-    url: URL # URL before readFromFd
-    pstream: SocketStream # control stream
-    savetask: bool
-    ishtml: bool
     firstBufferRead: bool
-    lines: FlexibleGrid
+    hoverText: array[HoverType, string]
+    htmlParser: HTML5ParserWrapper
     images: seq[PosBitmap]
-    attrs: WindowAttributes
-    window: Window
-    document: Document
-    prevStyled: StyledNode
-    selector: Selector[int]
+    ishtml: bool
     istream: PosixStream
-    bytesRead: int
+    lines: FlexibleGrid
+    loader: FileLoader
+    needsBOMSniff: bool
+    outputId: int
+    pollData: PollData
+    prevStyled: StyledNode
+    prevnode: StyledNode
+    pstream: SocketStream # control stream
+    quirkstyle: CSSStylesheet
     reportedBytesRead: int
+    rfd: int # file descriptor of command pipe
+    savetask: bool
+    ssock: ServerSocket
     state: BufferState
-    prevnode: StyledNode
-    loader: FileLoader
-    config: BufferConfig
     tasks: array[BufferCommand, int] #TODO this should have arguments
-    hoverText: array[HoverType, string]
-    estream: DynFileStream # error stream
-    ssock: ServerSocket
-    factory: CAtomFactory
     uastyle: CSSStylesheet
-    quirkstyle: CSSStylesheet
+    url: URL # URL before readFromFd
     userstyle: CSSStylesheet
-    htmlParser: HTML5ParserWrapper
-    bgcolor: CellColor
-    needsBOMSniff: bool
-    ctx: TextDecoderContext
-    charsetStack: seq[Charset]
-    charset: Charset
-    cacheId: int
-    outputId: int
-    emptySel: Selector[int]
+    window: Window
 
   InterfaceOpaque = ref object
     stream: SocketStream
@@ -900,24 +899,16 @@ proc rewind(buffer: Buffer; offset: int; unregister = true): bool =
     return false
   buffer.loader.resume(response.outputId)
   if unregister:
-    buffer.selector.unregister(buffer.fd)
+    buffer.pollData.unregister(buffer.fd)
     buffer.loader.unregistered.add(buffer.fd)
   buffer.istream.sclose()
   buffer.istream = response.body
   buffer.istream.setBlocking(false)
   buffer.fd = response.body.fd
-  buffer.selector.registerHandle(buffer.fd, {Read}, 0)
+  buffer.pollData.register(buffer.fd, POLLIN)
   buffer.bytesRead = offset
   return true
 
-# As defined in std/selectors: this determines whether kqueue is being used.
-# On these platforms, we must not close the selector after fork, since kqueue
-# fds are not inherited after a fork.
-const bsdPlatform = defined(macosx) or defined(freebsd) or defined(netbsd) or
-  defined(openbsd) or defined(dragonfly)
-
-proc onload(buffer: Buffer)
-
 when defined(freebsd) or defined(openbsd):
   # necessary for an ugly hack we will do later
   import std/kqueue
@@ -951,33 +942,7 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} =
     let sockFd = buffer.pstream.recvFileHandle()
     discard close(pipefd[0]) # close read
     let ps = newPosixStream(pipefd[1])
-    # We must allocate a new selector for this new process. (Otherwise we
-    # would interfere with operation of the other one.)
-    # Closing seems to suffice here.
-    when not bsdPlatform:
-      buffer.selector.close()
-    when defined(freebsd) or defined(openbsd):
-      # Hack necessary because newSelector calls sysctl, but Capsicum really
-      # dislikes that and we don't want to request sysctl capabilities
-      # from pledge either.
-      #
-      # To make this work we
-      # * allocate a new Selector object on buffer startup
-      # * copy into it the initial state of the real selector we will use
-      # * on fork, reset the selector object's state by writing the dummy
-      #   selector into it
-      # * override the file handle with a new kqueue().
-      #
-      # Warning: this breaks when threading is enabled; then fds is no longer a
-      # seq, so it's copied by reference (+ leaks). We explicitly disable
-      # threading, so for now we should be fine.
-      let fd = kqueue()
-      doAssert fd != -1
-      buffer.selector[] = buffer.emptySel[]
-      cast[ptr cint](buffer.selector)[] = fd
-    else:
-      buffer.selector = newSelector[int]()
-    #TODO set buffer.window.timeouts.selector
+    buffer.pollData.clear()
     var connecting: seq[ConnectData] = @[]
     var ongoing: seq[OngoingData] = @[]
     for it in buffer.loader.data:
@@ -1000,7 +965,7 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} =
       response.body = stream
       let data = OngoingData(response: response, stream: stream)
       let fd = data.fd
-      buffer.selector.registerHandle(fd, {Read}, 0)
+      buffer.pollData.register(fd, POLLIN)
       buffer.loader.put(data)
     if buffer.istream != nil:
       # We do not own our input stream, so we can't tee it.
@@ -1025,7 +990,7 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} =
     var r = buffer.pstream.initPacketReader()
     r.sread(buffer.loader.key)
     buffer.rfd = buffer.pstream.fd
-    buffer.selector.registerHandle(buffer.rfd, {Read}, 0)
+    buffer.pollData.register(buffer.rfd, POLLIN)
     # must reconnect after the new client is set up, or the client pids get
     # mixed up.
     for it in connecting:
@@ -1070,7 +1035,7 @@ proc finishLoad(buffer: Buffer): EmptyPromise =
   buffer.document.readyState = rsInteractive
   if buffer.config.scripting:
     buffer.dispatchDOMContentLoadedEvent()
-  buffer.selector.unregister(buffer.fd)
+  buffer.pollData.unregister(buffer.fd)
   buffer.loader.unregistered.add(buffer.fd)
   buffer.loader.removeCachedItem(buffer.cacheId)
   buffer.cacheId = -1
@@ -1181,12 +1146,12 @@ proc cancel*(buffer: Buffer) {.proxy.} =
     return
   for it in buffer.loader.data:
     let fd = it.fd
-    buffer.selector.unregister(fd)
+    buffer.pollData.unregister(fd)
     buffer.loader.unregistered.add(fd)
     it.stream.sclose()
     buffer.loader.unset(it)
   if buffer.istream != nil:
-    buffer.selector.unregister(buffer.fd)
+    buffer.pollData.unregister(buffer.fd)
     buffer.loader.unregistered.add(buffer.fd)
     buffer.loader.removeCachedItem(buffer.cacheId)
     buffer.fd = -1
@@ -1397,8 +1362,8 @@ proc click(buffer: Buffer; label: HTMLLabelElement): ClickResult =
 
 proc click(buffer: Buffer; select: HTMLSelectElement): ClickResult =
   let repaint = buffer.setFocus(select)
-  var options: seq[string]
-  var selected: seq[int]
+  var options: seq[string] = @[]
+  var selected: seq[int] = @[]
   var i = 0
   for option in select.options:
     options.add(option.textContent.stripAndCollapse())
@@ -1806,7 +1771,7 @@ proc handleRead(buffer: Buffer; fd: int): bool =
     assert false
   true
 
-proc handleError(buffer: Buffer; fd: int; err: OSErrorCode): bool =
+proc handleError(buffer: Buffer; fd: int): bool =
   if fd == buffer.rfd:
     # Connection reset by peer, probably. Close the buffer.
     return false
@@ -1815,32 +1780,36 @@ proc handleError(buffer: Buffer; fd: int; err: OSErrorCode): bool =
   elif buffer.loader.get(fd) != nil:
     if not buffer.loader.onError(fd):
       #TODO handle connection error
-      assert false, $fd & ": " & $err
+      assert false, $fd
     if buffer.config.scripting:
       buffer.window.runJSJobs()
   elif fd in buffer.loader.unregistered:
     discard # ignore
   else:
-    assert false, $fd & ": " & $err
+    assert false, $fd
   true
 
+proc getPollTimeout(buffer: Buffer): cint =
+  if not buffer.config.scripting:
+    return -1
+  return buffer.window.timeouts.sortAndGetTimeout()
+
 proc runBuffer(buffer: Buffer) =
   var alive = true
-  var keys: array[64, ReadyKey]
   while alive:
-    let count = buffer.selector.selectInto(-1, keys)
-    for event in keys.toOpenArray(0, count - 1):
-      if Read in event.events:
+    let timeout = buffer.getPollTimeout()
+    buffer.pollData.poll(timeout)
+    for event in buffer.pollData.events:
+      if (event.revents and POLLIN) != 0:
         if not buffer.handleRead(event.fd):
           alive = false
           break
-      if Error in event.events:
-        if not buffer.handleError(event.fd, event.errorCode):
+      if (event.revents and POLLERR) != 0 or (event.revents and POLLHUP) != 0:
+        if not buffer.handleError(event.fd):
           alive = false
           break
-      if selectors.Event.Timer in event.events:
-        let r = buffer.window.timeouts.runTimeoutFd(event.fd)
-        assert r
+    if buffer.config.scripting:
+      if buffer.window.timeouts.run():
         buffer.window.runJSJobs()
         buffer.maybeReshape()
     buffer.loader.unregistered.setLen(0)
@@ -1853,9 +1822,7 @@ proc cleanup(buffer: Buffer) =
 
 proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
     ishtml: bool; charsetStack: seq[Charset]; loader: FileLoader;
-    ssock: ServerSocket; pstream: SocketStream; selector: Selector[int]) =
-  let emptySel = Selector[int]()
-  emptySel[] = selector[]
+    ssock: ServerSocket; pstream: SocketStream) =
   let factory = newCAtomFactory()
   let confidence = if config.charsetOverride == CHARSET_UNKNOWN:
     ccTentative
@@ -1870,16 +1837,14 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
     needsBOMSniff: config.charsetOverride == CHARSET_UNKNOWN,
     pstream: pstream,
     rfd: pstream.fd,
-    selector: selector,
     ssock: ssock,
     url: url,
     charsetStack: charsetStack,
     cacheId: -1,
     outputId: -1,
-    emptySel: emptySel,
     factory: factory,
-    window: newWindow(config.scripting, config.images, config.styling, selector,
-      attrs, factory, loader, url)
+    window: newWindow(config.scripting, config.images, config.styling, attrs,
+      factory, loader, url)
   )
   if buffer.config.scripting:
     buffer.window.navigate = proc(url: URL) = buffer.navigate(url)
@@ -1891,12 +1856,12 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
   buffer.fd = int(fd)
   buffer.istream = newPosixStream(fd)
   buffer.istream.setBlocking(false)
-  buffer.selector.registerHandle(int(fd), {Read}, 0)
+  buffer.pollData.register(fd, POLLIN)
   loader.registerFun = proc(fd: int) =
-    buffer.selector.registerHandle(fd, {Read}, 0)
+    buffer.pollData.register(fd, POLLIN)
   loader.unregisterFun = proc(fd: int) =
-    buffer.selector.unregister(fd)
-  buffer.selector.registerHandle(buffer.rfd, {Read}, 0)
+    buffer.pollData.unregister(fd)
+  buffer.pollData.register(buffer.rfd, POLLIN)
   const css = staticRead"res/ua.css"
   const quirk = css & staticRead"res/quirk.css"
   buffer.initDecoder()
diff --git a/src/server/forkserver.nim b/src/server/forkserver.nim
index eaea5075..7c6997a8 100644
--- a/src/server/forkserver.nim
+++ b/src/server/forkserver.nim
@@ -1,7 +1,6 @@
 import std/options
 import std/os
 import std/posix
-import std/selectors
 import std/tables
 
 import chagashi/charset
@@ -149,10 +148,6 @@ proc forkBuffer(ctx: var ForkServerContext; r: var BufferedReader): int =
     discard close(pipefd[0]) # close read
     closeStdin()
     closeStdout()
-    # must call before entering the sandbox, or capsicum cries because of Nim
-    # calling sysctl
-    # also lets us deny sysctl call with pledge
-    let selector = newSelector[int]()
     setBufferProcessTitle(url)
     let pid = getCurrentProcessId()
     let ssock = initServerSocket(sockDir, sockDirFd, pid)
@@ -178,7 +173,7 @@ proc forkBuffer(ctx: var ForkServerContext; r: var BufferedReader): int =
     )
     try:
       launchBuffer(config, url, attrs, ishtml, charsetStack, loader,
-        ssock, pstream, selector)
+        ssock, pstream)
     except CatchableError:
       let e = getCurrentException()
       # taken from system/excpt.nim
diff --git a/src/utils/sandbox.nim b/src/utils/sandbox.nim
index 9e0498a5..f7afbb91 100644
--- a/src/utils/sandbox.nim
+++ b/src/utils/sandbox.nim
@@ -175,14 +175,10 @@ elif SandboxMode == stLibSeccomp:
       "clone", # for when fork is implemented as clone
       "close", # duh
       "connect", # for outgoing requests to loader
-      "epoll_create", "epoll_create1", "epoll_ctl", "epoll_wait", # epoll stuff
-      "epoll_pwait", # for bionic & musl
-      "eventfd", # used by Nim selectors
       "exit_group", # for quit
       "fork", # for when fork is really fork
       "futex", # bionic libc & WSL both need it
       "getpid", # for determining current PID after we fork
-      "getrlimit", # glibc uses it after fork it seems
       "getsockname", # Nim needs it for connecting
       "gettimeofday", # used by QuickJS in Date.now()
       "lseek", # glibc calls lseek on open files at exit
@@ -192,17 +188,12 @@ elif SandboxMode == stLibSeccomp:
       "munmap", # memory allocation
       "pipe", # for pipes to child process
       "pipe2", # for when pipe is implemented as pipe2
-      "prlimit64", # for when getrlimit is implemented as prlimit64
+      "poll", "ppoll", # for polling (sometimes implemented as ppoll, see musl)
       "read", "recv", "recvfrom", "recvmsg", # for reading from sockets
       "rt_sigreturn", # for when sigreturn is implemented as rt_sigreturn
       "send", "sendmsg", "sendto", # for writing to sockets
       "set_robust_list", # glibc seems to need it for whatever reason
-      "setrlimit", # glibc seems to use it for whatever reason
       "sigreturn", # called by signal trampoline
-      "timerfd_create", # used by Nim selectors
-      "timerfd_gettime", # not actually used by Nim but may be in the future
-      "timerfd_settime", # used by Nim selectors
-      "ugetrlimit", # glibc uses it after fork it seems
       "write" # for writing to sockets
     ]
     for it in allowList:
@@ -235,7 +226,7 @@ elif SandboxMode == stLibSeccomp:
       "read", "write", "recv", "send", "recvfrom", "sendto", # socket i/o
       "lseek", # glibc calls lseek on open files at exit
       "mmap", "mmap2", "mremap", "munmap", "brk", # memory allocation
-      "poll", # curl needs poll
+      "poll", "ppoll", # curl needs poll
       "getpid", # used indirectly by OpenSSL EVP_RAND_CTX_new (through drbg)
       "futex", # bionic libc & WSL both need it
       # we either have to use CURLOPT_NOSIGNAL or allow signals.