diff options
author | bptato <nincsnevem662@gmail.com> | 2024-09-23 19:51:20 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-09-23 19:58:54 +0200 |
commit | fcd9aa9f9c604ed5d104343542962a26b2acda62 (patch) | |
tree | 7b0eea9b63bacc27cdc6471e2b2409b7b5d15c9e | |
parent | 8e8c7f0911f4a20446a83090d722fecaf203f6f3 (diff) | |
download | chawan-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).
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | adapter/format/ansi2html.nim | 35 | ||||
-rw-r--r-- | src/html/env.nim | 33 | ||||
-rw-r--r-- | src/io/bufreader.nim | 1 | ||||
-rw-r--r-- | src/io/bufwriter.nim | 1 | ||||
-rw-r--r-- | src/io/dynstream.nim | 9 | ||||
-rw-r--r-- | src/io/poll.nim | 40 | ||||
-rw-r--r-- | src/js/timeout.nim | 113 | ||||
-rw-r--r-- | src/loader/loader.nim | 63 | ||||
-rw-r--r-- | src/local/client.nim | 130 | ||||
-rw-r--r-- | src/local/pager.nim | 11 | ||||
-rw-r--r-- | src/server/buffer.nim | 161 | ||||
-rw-r--r-- | src/server/forkserver.nim | 7 | ||||
-rw-r--r-- | src/utils/sandbox.nim | 13 |
14 files changed, 316 insertions, 303 deletions
diff --git a/Makefile b/Makefile index 80a59e4b..1b5b7857 100644 --- a/Makefile +++ b/Makefile @@ -119,7 +119,7 @@ $(OUTDIR_CGI_BIN)/resize: adapter/img/stb_image_resize.h adapter/img/stb_image_r src/utils/sandbox.nim $(dynstream) $(twtstr) $(OUTDIR_LIBEXEC)/urlenc: $(twtstr) $(OUTDIR_LIBEXEC)/gopher2html: adapter/gophertypes.nim $(twtstr) -$(OUTDIR_LIBEXEC)/ansi2html: src/types/color.nim $(twtstr) +$(OUTDIR_LIBEXEC)/ansi2html: src/types/color.nim src/io/poll.nim $(twtstr) $(dynstream) $(OUTDIR_LIBEXEC)/md2html: $(twtstr) $(OUTDIR_CGI_BIN)/%: adapter/protocol/%.nim diff --git a/adapter/format/ansi2html.nim b/adapter/format/ansi2html.nim index dcbc4210..9f24dd0b 100644 --- a/adapter/format/ansi2html.nim +++ b/adapter/format/ansi2html.nim @@ -1,8 +1,9 @@ import std/options import std/os import std/posix -import std/selectors +import io/dynstream +import io/poll import types/color import utils/twtstr @@ -381,27 +382,23 @@ proc main() = if standalone: state.puts("<body>\n") state.puts("<pre style='margin: 0'>\n") - let ofl = fcntl(STDIN_FILENO, F_GETFL, 0) - doAssert ofl != -1 - discard fcntl(STDIN_FILENO, F_SETFL, ofl and not O_NONBLOCK) + let ps = newPosixStream(STDIN_FILENO) + ps.setBlocking(false) var buffer {.noinit.}: array[4096, char] - var selector = newSelector[int]() - block mainloop: - while true: - let n = read(STDIN_FILENO, addr buffer[0], buffer.high) - if n != -1: - if n == 0: - break - state.processData(buffer.toOpenArray(0, n - 1)) - else: - doAssert errno == EAGAIN or errno == EWOULDBLOCK - state.flushOutbuf() - selector.registerHandle(STDIN_FILENO, {Read}, 0) - discard selector.select(-1) - selector.unregister(STDIN_FILENO) + var pollData = PollData() + while true: + try: + let n = ps.recvData(buffer) + if n == 0: + break + state.processData(buffer.toOpenArray(0, n - 1)) + except ErrorAgain: + state.flushOutbuf() + pollData.register(ps.fd, POLLIN) + pollData.poll(-1) + pollData.unregister(ps.fd) if standalone: state.puts("</body>") state.flushOutbuf() - discard fcntl(STDIN_FILENO, F_SETFL, ofl) main() 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. |