diff options
Diffstat (limited to 'src')
-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 |
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. |