diff options
-rw-r--r-- | src/buffer/buffer.nim | 109 | ||||
-rw-r--r-- | src/display/client.nim | 110 | ||||
-rw-r--r-- | src/html/dom.nim | 2 | ||||
-rw-r--r-- | src/html/env.nim | 70 | ||||
-rw-r--r-- | src/ips/forkserver.nim | 2 | ||||
-rw-r--r-- | src/js/javascript.nim | 7 | ||||
-rw-r--r-- | src/js/timeout.nim | 104 |
7 files changed, 256 insertions, 148 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim index b0dc0696..7fda2887 100644 --- a/src/buffer/buffer.nim +++ b/src/buffer/buffer.nim @@ -32,6 +32,7 @@ import ips/serialize import ips/serversocket import ips/socketstream import js/regex +import js/timeout import io/window import layout/box import render/renderdocument @@ -70,7 +71,9 @@ type str*: string Buffer* = ref object - fd: int + rfd: int # file descriptor of command pipe + fd: int # file descriptor of buffer source + oldfd: int # fd after being unregistered alive: bool readbufsize: int contenttype: string @@ -97,7 +100,6 @@ type loader: FileLoader config: BufferConfig userstyle: CSSStylesheet - timeouts: Table[int, (proc())] tasks: array[BufferCommand, int] #TODO this should have arguments savetask: bool hovertext: array[HoverType, string] @@ -649,7 +651,7 @@ proc finishLoad(buffer: Buffer): EmptyPromise = buffer.sstream.setPosition(0) buffer.available = 0 if buffer.window == nil: - buffer.window = newWindow(buffer.config.scripting) + buffer.window = newWindow(buffer.config.scripting, buffer.selector) let doc = parseHTML(buffer.sstream, charsets = buffer.charsets, window = buffer.window, url = buffer.url) buffer.document = doc @@ -659,6 +661,7 @@ proc finishLoad(buffer: Buffer): EmptyPromise = p = EmptyPromise() p.resolve() buffer.selector.unregister(buffer.fd) + buffer.oldfd = buffer.fd buffer.fd = -1 buffer.istream.close() return p @@ -741,7 +744,7 @@ proc cancel*(buffer: Buffer): int {.proxy.} = buffer.sstream.setPosition(0) buffer.available = 0 if buffer.window == nil: - buffer.window = newWindow(buffer.config.scripting) + buffer.window = newWindow(buffer.config.scripting, buffer.selector) buffer.document = parseHTML(buffer.sstream, charsets = buffer.charsets, window = buffer.window, url = buffer.url, canReinterpret = false) @@ -1168,51 +1171,58 @@ proc readCommand(buffer: Buffer) = buffer.pstream.sread(packetid) bufferDispatcher(ProxyFunctions, buffer, cmd, packetid) +proc handleRead(buffer: Buffer, fd: int) = + if fd == buffer.rfd: + try: + buffer.readCommand() + except EOFError: + #eprint "EOF error", $buffer.url & "\nMESSAGE:", + # getCurrentExceptionMsg() & "\n", + # getStackTrace(getCurrentException()) + buffer.alive = false + elif fd == buffer.fd: + buffer.onload() + elif fd in buffer.loader.connecting: + buffer.loader.onConnected(fd) + elif fd in buffer.loader.ongoing: + #TODO something with readablestream? + discard + elif buffer.fd == -1 and buffer.oldfd == fd: + discard #TODO hack + else: assert false + +proc handleError(buffer: Buffer, fd: int) = + if fd == buffer.rfd: + # Connection reset by peer, probably. Close the buffer. + buffer.alive = false + elif fd == buffer.fd: + buffer.onload() + elif fd in buffer.loader.connecting: + # probably shouldn't happen. TODO + assert false + elif fd in buffer.loader.ongoing: + #TODO something with readablestream? + discard + elif buffer.fd == -1 and fd == buffer.oldfd: + discard #TODO hack + else: + assert false + proc runBuffer(buffer: Buffer, rfd: int) = - block loop: - while buffer.alive: - let events = buffer.selector.select(-1) - for event in events: - if event.fd == rfd: - if Error in event.events: - # Connection reset by peer, probably. Close the buffer. - break loop - elif Read in event.events: - try: - buffer.readCommand() - except EOFError: - #eprint "EOF error", $buffer.url & "\nMESSAGE:", - # getCurrentExceptionMsg() & "\n", - # getStackTrace(getCurrentException()) - break loop - else: - assert false - elif event.fd == buffer.fd: - if Read in event.events or Error in event.events: - buffer.onload() - else: - assert false - elif event.fd in buffer.loader.connecting: - if Event.Read in event.events: - buffer.loader.onConnected(event.fd) - else: - # probably shouldn't happen. TODO: maybe with Error? - assert false - elif event.fd in buffer.loader.ongoing: - #TODO something with readablestream? - discard - elif event.fd in buffer.timeouts: - if Event.Timer in event.events: - buffer.selector.unregister(event.fd) - var timeout: proc() - if buffer.timeouts.pop(event.fd, timeout): - timeout() - else: - assert false - else: - assert false - else: - assert false + buffer.rfd = rfd + while buffer.alive: + let events = buffer.selector.select(-1) + for event in events: + if Error in event.events: + buffer.handleError(event.fd) + if not buffer.alive: + break + if Read in event.events: + buffer.handleRead(event.fd) + if Event.Timer in event.events: + assert buffer.window != nil + assert buffer.window.timeouts.runTimeoutFd(event.fd) + buffer.window.runJSJobs() buffer.pstream.close() buffer.loader.quit() quit(0) @@ -1238,7 +1248,8 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource, loader.unregisterFun = proc(fd: int) = buffer.selector.unregister(fd) buffer.srenderer = newStreamRenderer(buffer.sstream, buffer.charsets) if buffer.config.scripting: - buffer.window = newWindow(buffer.config.scripting, some(buffer.loader)) + buffer.window = newWindow(buffer.config.scripting, buffer.selector, + some(buffer.loader)) let socks = connectSocketStream(mainproc, false) socks.swrite(getpid()) buffer.pstream = socks diff --git a/src/display/client.nim b/src/display/client.nim index 5067ced7..de09885c 100644 --- a/src/display/client.nim +++ b/src/display/client.nim @@ -31,6 +31,7 @@ import ips/serialize import ips/serversocket import ips/socketstream import js/javascript +import js/timeout import types/cookie import types/dispatcher import types/url @@ -52,12 +53,8 @@ type config {.jsget.}: Config jsrt: JSRuntime jsctx: JSContext - timeoutid: int - timeouts: Table[int, tuple[handler: (proc()), fdi: int]] - intervals: Table[int, tuple[handler: (proc()), fdi: int, tofree: JSValue]] - timeout_fdis: Table[int, int] - interval_fdis: Table[int, int] fdmap: Table[int, Container] + timeouts: TimeoutState[Container] ssock: ServerSocket selector: Selector[Container] @@ -105,11 +102,7 @@ proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} = return 0 proc runJSJobs(client: Client) = - while JS_IsJobPending(client.jsrt): - var ctx: JSContext - let r = JS_ExecutePendingJob(client.jsrt, addr ctx) - if r == -1: - ctx.writeException(client.console.err) + client.jsrt.runJSJobs(client.console.err) proc evalJS(client: Client, src, filename: string): JSValue = if client.console.tty != nil: @@ -202,59 +195,19 @@ proc input(client: Client) = client.feednext = false client.s = "" -proc setTimeout[T: JSValue|string](client: Client, handler: T, timeout = 0): int {.jsfunc.} = - let id = client.timeoutid - inc client.timeoutid - let fdi = client.selector.registerTimer(max(timeout, 1), true, nil) - client.timeout_fdis[fdi] = id - when T is string: - client.timeouts[id] = ((proc() = - client.evalJSFree(handler, "setTimeout handler") - ), fdi) - else: - let fun = JS_DupValue(client.jsctx, handler) - client.timeouts[id] = ((proc() = - let ret = JS_Call(client.jsctx, fun, JS_UNDEFINED, 0, nil) - if JS_IsException(ret): - client.jsctx.writeException(client.console.err) - JS_FreeValue(client.jsctx, ret) - JS_FreeValue(client.jsctx, fun) - ), fdi) - return id - -proc setInterval[T: JSValue|string](client: Client, handler: T, interval = 0): int {.jsfunc.} = - let id = client.timeoutid - inc client.timeoutid - let fdi = client.selector.registerTimer(max(interval, 1), false, nil) - client.interval_fdis[fdi] = id - when T is string: - client.intervals[id] = ((proc() = - client.evalJSFree(handler, "setInterval handler") - ), fdi, JS_NULL) - else: - let fun = JS_DupValue(client.jsctx, handler) - client.intervals[id] = ((proc() = - let ret = JS_Call(client.jsctx, handler, JS_UNDEFINED, 0, nil) - if JS_IsException(ret): - client.jsctx.writeException(client.console.err) - JS_FreeValue(client.jsctx, ret) - ), fdi, fun) - return id - -proc clearTimeout(client: Client, id: int) {.jsfunc.} = - if id in client.timeouts: - let timeout = client.timeouts[id] - client.selector.unregister(timeout.fdi) - client.timeout_fdis.del(timeout.fdi) - client.timeouts.del(id) - -proc clearInterval(client: Client, id: int) {.jsfunc.} = - if id in client.intervals: - let interval = client.intervals[id] - client.selector.unregister(interval.fdi) - JS_FreeValue(client.jsctx, interval.tofree) - client.interval_fdis.del(interval.fdi) - client.intervals.del(id) +proc setTimeout[T: JSValue|string](client: Client, handler: T, + timeout = 0i32): int32 {.jsfunc.} = + return client.timeouts.setTimeout(handler, timeout) + +proc setInterval[T: JSValue|string](client: Client, handler: T, + interval = 0i32): int32 {.jsfunc.} = + return client.timeouts.setInterval(handler, interval) + +proc clearTimeout(client: Client, id: int32) {.jsfunc.} = + client.timeouts.clearTimeout(id) + +proc clearInterval(client: Client, id: int32) {.jsfunc.} = + client.timeouts.clearInterval(id) let SIGWINCH {.importc, header: "<signal.h>", nodecl.}: cint @@ -390,15 +343,10 @@ proc inputLoop(client: Client) = client.attrs = getWindowAttributes(client.console.tty) client.pager.windowChange(client.attrs) if Event.Timer in event.events: - if event.fd in client.interval_fdis: - client.intervals[client.interval_fdis[event.fd]].handler() - client.runJSJobs() - elif event.fd in client.timeout_fdis: - let id = client.timeout_fdis[event.fd] - let timeout = client.timeouts[id] - timeout.handler() - client.clearTimeout(id) - client.runJSJobs() + assert client.timeouts.runTimeoutFd(event.fd) + client.runJSJobs() + client.console.container.requestLines().then(proc() = + client.console.container.cursorLastLine()) if client.pager.scommand != "": client.command(client.pager.scommand) client.pager.scommand = "" @@ -411,8 +359,7 @@ proc inputLoop(client: Client) = client.pager.draw() func hasSelectFds(client: Client): bool = - return client.timeouts.len > 0 or - client.intervals.len > 0 or + return not client.timeouts.empty or client.pager.numload > 0 or client.loader.connecting.len > 0 or client.loader.ongoing.len > 0 @@ -426,15 +373,8 @@ proc headlessLoop(client: Client) = if Error in event.events: client.handleError(event.fd) if Event.Timer in event.events: - if event.fd in client.interval_fdis: - client.intervals[client.interval_fdis[event.fd]].handler() - client.runJSJobs() - elif event.fd in client.timeout_fdis: - let id = client.timeout_fdis[event.fd] - let timeout = client.timeouts[id] - timeout.handler() - client.clearTimeout(id) - client.runJSJobs() + assert client.timeouts.runTimeoutFd(event.fd) + client.runJSJobs() client.acceptBuffers() #TODO this is dumb @@ -504,6 +444,10 @@ proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], client.selector.registerHandle(int(client.dispatcher.forkserver.estream.fd), {Read}, nil) client.pager.launchPager(tty) client.console = newConsole(client.pager, tty) + #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.alive = true addExitProc((proc() = client.quit())) if client.config.start.startup_script != "": diff --git a/src/html/dom.nim b/src/html/dom.nim index fef66e2d..4bc76a0a 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -13,6 +13,7 @@ import html/tags import io/loader import io/request import js/javascript +import js/timeout import types/mime import types/referer import types/url @@ -83,6 +84,7 @@ type jsrt*: JSRuntime jsctx*: JSContext document* {.jsget.}: Document + timeouts*: TimeoutState[int] # Navigator stuff Navigator* = ref object diff --git a/src/html/env.nim b/src/html/env.nim index 58bad005..bda8c2c2 100644 --- a/src/html/env.nim +++ b/src/html/env.nim @@ -1,3 +1,4 @@ +import selectors import streams import html/dom @@ -6,6 +7,7 @@ import io/loader import io/promise import io/request import js/javascript +import js/timeout import types/url # NavigatorID @@ -57,8 +59,56 @@ proc fetch(window: Window, req: Request): Promise[Response] {.jsfunc.} = if window.loader.isSome: return window.loader.get.fetch(req) -proc newWindow*(scripting: bool, loader = none(FileLoader)): Window = - result = Window( +proc setTimeout[T: JSValue|string](window: Window, handler: T, + timeout = 0i32): int32 {.jsfunc.} = + return window.timeouts.setTimeout(handler, timeout) + +proc setInterval[T: JSValue|string](window: Window, handler: T, + interval = 0i32): int32 {.jsfunc.} = + return window.timeouts.setInterval(handler, interval) + +proc clearTimeout(window: Window, id: int32) {.jsfunc.} = + window.timeouts.clearTimeout(id) + +proc clearInterval(window: Window, id: int32) {.jsfunc.} = + window.timeouts.clearInterval(id) + +proc addScripting*(window: Window, selector: Selector[int]) = + let rt = newJSRuntime() + let ctx = rt.newJSContext() + window.jsrt = rt + window.jsctx = ctx + 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): + let ss = newStringStream() + window.jsctx.writeException(ss) + ss.setPosition(0) + window.console.log("Exception in document", $window.document.url, + ss.readAll()) + ) + ) + var global = JS_GetGlobalObject(ctx) + ctx.registerType(Window, asglobal = true) + ctx.setOpaque(global, window) + ctx.setProperty(global, "window", global) + JS_FreeValue(ctx, global) + ctx.addconsoleModule() + ctx.addNavigatorModule() + ctx.addDOMModule() + ctx.addURLModule() + ctx.addHTMLModule() + +proc runJSJobs*(window: Window) = + window.jsrt.runJSJobs(window.console.err) + +proc newWindow*(scripting: bool, selector: Selector[int], + loader = none(FileLoader)): Window = + let window = Window( console: console(err: newFileStream(stderr)), navigator: Navigator(), loader: loader, @@ -67,17 +117,5 @@ proc newWindow*(scripting: bool, loader = none(FileLoader)): Window = ) ) if scripting: - let rt = newJSRuntime() - let ctx = rt.newJSContext() - result.jsrt = rt - result.jsctx = ctx - var global = JS_GetGlobalObject(ctx) - ctx.registerType(Window, asglobal = true) - ctx.setOpaque(global, result) - ctx.setProperty(global, "window", global) - JS_FreeValue(ctx, global) - ctx.addconsoleModule() - ctx.addNavigatorModule() - ctx.addDOMModule() - ctx.addURLModule() - ctx.addHTMLModule() + window.addScripting(selector) + return window diff --git a/src/ips/forkserver.nim b/src/ips/forkserver.nim index c6acefc6..646d7c39 100644 --- a/src/ips/forkserver.nim +++ b/src/ips/forkserver.nim @@ -79,6 +79,7 @@ proc forkLoader(ctx: var ForkServerContext, config: LoaderConfig): Pid = let msg = e.getStackTrace() & "Error: unhandled exception: " & e.msg & " [" & $e.name & "]\n" stderr.write(msg) + quit(1) doAssert false let readfd = pipefd[0] # get read discard close(pipefd[1]) # close write @@ -125,6 +126,7 @@ proc forkBuffer(ctx: var ForkServerContext): Pid = let msg = e.getStackTrace() & "Error: unhandled exception: " & e.msg & " [" & $e.name & "]\n" stderr.write(msg) + quit(1) doAssert false ctx.children.add((pid, loaderPid)) return pid diff --git a/src/js/javascript.nim b/src/js/javascript.nim index 438858ef..c6459956 100644 --- a/src/js/javascript.nim +++ b/src/js/javascript.nim @@ -245,6 +245,13 @@ proc writeException*(ctx: JSContext, s: Stream) = JS_FreeValue(ctx, stack) JS_FreeValue(ctx, ex) +proc runJSJobs*(rt: JSRuntime, err: Stream) = + while JS_IsJobPending(rt): + var ctx: JSContext + let r = JS_ExecutePendingJob(rt, addr ctx) + if r == -1: + ctx.writeException(err) + func isInstanceOf*(ctx: JSContext, obj: JSValue, class: string): bool = let clazz = ctx.getClass(class) if clazz in ctx.getOpaque().ctors: diff --git a/src/js/timeout.nim b/src/js/timeout.nim new file mode 100644 index 00000000..0e31d8a3 --- /dev/null +++ b/src/js/timeout.nim @@ -0,0 +1,104 @@ +import selectors +import streams +import tables + +import js/javascript + +type TimeoutState*[T] = object + timeoutid: int32 + timeouts: Table[int32, tuple[handler: (proc()), fdi: int]] + intervals: Table[int32, tuple[handler: (proc()), fdi: int, tofree: JSValue]] + timeout_fdis: Table[int, int32] + interval_fdis: Table[int, int32] + selector: Selector[T] #TODO would be better with void... + jsctx: JSContext + err: Stream #TODO shouldn't be needed + evalJSFree: proc(src, file: string) #TODO ew + +func newTimeoutState*[T](selector: Selector[T], jsctx: JSContext, + err: Stream, evalJSFree: proc(src, file: string)): TimeoutState[T] = + return TimeoutState[T]( + selector: selector, + jsctx: jsctx, + err: err, + evalJSFree: evalJSFree + ) + +func empty*(state: TimeoutState): bool = + return state.timeouts.len == 0 and state.intervals.len == 0 + +#TODO varargs +proc setTimeout*[T: JSValue|string, S](state: var TimeoutState[S], handler: T, + timeout = 0i32): int32 = + let id = state.timeoutid + inc state.timeoutid + let fdi = state.selector.registerTimer(max(timeout, 1), true, default(S)) + state.timeout_fdis[fdi] = id + when T is string: + let evalJSFree = state.evalJSFree + state.timeouts[id] = ((proc() = + evalJSFree(handler, "setTimeout handler") + ), fdi) + else: + let fun = JS_DupValue(state.jsctx, handler) + let jsctx = state.jsctx + let err = state.err + state.timeouts[id] = ((proc() = + let ret = JS_Call(jsctx, fun, JS_UNDEFINED, 0, nil) + if JS_IsException(ret): + jsctx.writeException(err) + JS_FreeValue(jsctx, ret) + JS_FreeValue(jsctx, fun) + ), fdi) + return id + +proc clearTimeout*(state: var TimeoutState, id: int32) = + if id in state.timeouts: + let timeout = state.timeouts[id] + state.selector.unregister(timeout.fdi) + state.timeout_fdis.del(timeout.fdi) + state.timeouts.del(id) + +proc clearInterval*(state: var TimeoutState, id: int32) = + if id in state.intervals: + let interval = state.intervals[id] + state.selector.unregister(interval.fdi) + JS_FreeValue(state.jsctx, interval.tofree) + state.interval_fdis.del(interval.fdi) + state.intervals.del(id) + +#TODO varargs +proc setInterval*[T: JSValue|string, S](state: var TimeoutState[S], handler: T, + interval = 0i32): int32 = + let id = state.timeoutid + inc state.timeoutid + let fdi = state.selector.registerTimer(max(interval, 1), false, default(S)) + state.interval_fdis[fdi] = id + when T is string: + let evalJSFree = state.evalJSFree + state.intervals[id] = ((proc() = + evalJSFree(handler, "setInterval handler") + ), fdi, JS_NULL) + else: + let fun = JS_DupValue(state.jsctx, handler) + let jsctx = state.jsctx + let err = state.err + state.intervals[id] = ((proc() = + let ret = JS_Call(jsctx, handler, JS_UNDEFINED, 0, nil) + if JS_IsException(ret): + jsctx.writeException(err) + JS_FreeValue(jsctx, ret) + ), fdi, fun) + return id + +proc runTimeoutFd*(state: var TimeoutState, fd: int): bool = + if fd in state.interval_fdis: + state.intervals[state.interval_fdis[fd]].handler() + return true + elif fd in state.timeout_fdis: + let id = state.timeout_fdis[fd] + let timeout = state.timeouts[id] + timeout.handler() + state.clearTimeout(id) + return true + return false |