about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/buffer/buffer.nim109
-rw-r--r--src/display/client.nim110
-rw-r--r--src/html/dom.nim2
-rw-r--r--src/html/env.nim70
-rw-r--r--src/ips/forkserver.nim2
-rw-r--r--src/js/javascript.nim7
-rw-r--r--src/js/timeout.nim104
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