about summary refs log tree commit diff stats
path: root/src/local
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-09-23 19:51:20 +0200
committerbptato <nincsnevem662@gmail.com>2024-09-23 19:58:54 +0200
commitfcd9aa9f9c604ed5d104343542962a26b2acda62 (patch)
tree7b0eea9b63bacc27cdc6471e2b2409b7b5d15c9e /src/local
parent8e8c7f0911f4a20446a83090d722fecaf203f6f3 (diff)
downloadchawan-fcd9aa9f9c604ed5d104343542962a26b2acda62.tar.gz
Replace std/selectors with poll
std/selectors uses OS-specific selector APIs, which sounds good in
theory (faster than poll!), but sucks for portability in practice.
Sure, you can fix portability bugs, but who knows how many there are
on untested platforms... poll is standard, so if it works on one
computer it should work on all other ones. (I hope.)

As a bonus, I rewrote the timeout API for poll, which incidentally
fixes setTimeout across forks. Also, SIGWINCH should now work on all
platforms (as we self-pipe instead of signalfd/kqueue magic).
Diffstat (limited to 'src/local')
-rw-r--r--src/local/client.nim130
-rw-r--r--src/local/pager.nim11
2 files changed, 78 insertions, 63 deletions
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) =