about summary refs log tree commit diff stats
path: root/src/ips/forkserver.nim
blob: e8904354b1b0cc4dea12b7b234d86e853a11901e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import streams

when defined(posix):
  import posix

import buffer/buffer
import config/config
import io/loader
import io/request
import io/urlfilter
import io/window
import ips/serialize
import ips/serversocket
import types/buffersource
import types/cookie
import utils/twtstr

type
  ForkCommand* = enum
    FORK_BUFFER, FORK_LOADER, REMOVE_CHILD, LOAD_CONFIG

  ForkServer* = ref object
    process*: Pid
    istream*: Stream
    ostream*: Stream

  ForkServerContext = object
    istream: Stream
    ostream: Stream
    children: seq[(Pid, Pid)]

proc newFileLoader*(forkserver: ForkServer, defaultHeaders: HeaderList = DefaultHeaders, filter = newURLFilter(), cookiejar: CookieJar = nil): FileLoader =
  forkserver.ostream.swrite(FORK_LOADER)
  forkserver.ostream.swrite(defaultHeaders)
  forkserver.ostream.swrite(filter)
  forkserver.ostream.swrite(cookiejar)
  forkserver.ostream.flush()
  forkserver.istream.sread(result)

proc loadForkServerConfig*(forkserver: ForkServer, config: Config) =
  forkserver.ostream.swrite(LOAD_CONFIG)
  forkserver.ostream.swrite(config.getForkServerConfig())
  forkserver.ostream.flush()

proc removeChild*(forkserver: Forkserver, pid: Pid) =
  forkserver.ostream.swrite(REMOVE_CHILD)
  forkserver.ostream.swrite(pid)
  forkserver.ostream.flush()

proc forkLoader(ctx: var ForkServerContext, defaultHeaders: HeaderList, filter: URLFilter, cookiejar: CookieJar): FileLoader =
  var pipefd: array[2, cint]
  if pipe(pipefd) == -1:
    raise newException(Defect, "Failed to open pipe.")
  let pid = fork()
  if pid == 0:
    # child process
    for i in 0 ..< ctx.children.len: ctx.children[i] = (Pid(0), Pid(0))
    ctx.children.setLen(0)
    zeroMem(addr ctx, sizeof(ctx))
    discard close(pipefd[0]) # close read
    runFileLoader(pipefd[1], defaultHeaders, filter, cookiejar)
    assert false
  let readfd = pipefd[0] # get read
  discard close(pipefd[1]) # close write
  var readf: File
  if not open(readf, FileHandle(readfd), fmRead):
    raise newException(Defect, "Failed to open output handle.")
  assert readf.readChar() == char(0u8)
  close(readf)
  discard close(pipefd[0])
  return FileLoader(process: pid)

proc forkBuffer(ctx: var ForkServerContext): Pid =
  var source: BufferSource
  var config: BufferConfig
  var attrs: WindowAttributes
  var mainproc: Pid
  ctx.istream.sread(source)
  ctx.istream.sread(config)
  ctx.istream.sread(attrs)
  ctx.istream.sread(mainproc)
  let loader = ctx.forkLoader(DefaultHeaders, config.filter, config.cookiejar) #TODO make this configurable
  let pid = fork()
  if pid == 0:
    for i in 0 ..< ctx.children.len: ctx.children[i] = (Pid(0), Pid(0))
    ctx.children.setLen(0)
    zeroMem(addr ctx, sizeof(ctx))
    launchBuffer(config, source, attrs, loader, mainproc)
    assert false
  ctx.children.add((pid, loader.process))
  return pid

proc runForkServer() =
  var ctx: ForkServerContext
  ctx.istream = newFileStream(stdin)
  ctx.ostream = newFileStream(stdout)
  while true:
    try:
      var cmd: ForkCommand
      ctx.istream.sread(cmd)
      case cmd
      of REMOVE_CHILD:
        var pid: Pid
        ctx.istream.sread(pid)
        for i in 0 .. ctx.children.high:
          if ctx.children[i][0] == pid:
            ctx.children.del(i)
            break
      of FORK_BUFFER:
        ctx.ostream.swrite(ctx.forkBuffer())
      of FORK_LOADER:
        var defaultHeaders: HeaderList
        var filter: URLFilter
        var cookiejar: CookieJar
        ctx.istream.sread(defaultHeaders)
        ctx.istream.sread(filter)
        ctx.istream.sread(cookiejar)
        let loader = ctx.forkLoader(defaultHeaders, filter, cookiejar)
        ctx.ostream.swrite(loader)
        ctx.children.add((loader.process, Pid(-1)))
      of LOAD_CONFIG:
        var config: ForkServerConfig
        ctx.istream.sread(config)
        width_table = makewidthtable(config.ambiguous_double)
        SocketDirectory = config.tmpdir
      ctx.ostream.flush()
    except IOError:
      # EOF
      break
  ctx.istream.close()
  ctx.ostream.close()
  # Clean up when the main process crashed.
  for childpair in ctx.children:
    let a = childpair[0]
    let b = childpair[1]
    discard kill(cint(a), cint(SIGTERM))
    if b != -1:
      discard kill(cint(b), cint(SIGTERM))
  quit(0)

proc newForkServer*(): ForkServer =
  new(result)
  var pipefd_in: array[2, cint]
  var pipefd_out: array[2, cint]
  if pipe(pipefd_in) == -1:
    raise newException(Defect, "Failed to open input pipe.")
  if pipe(pipefd_out) == -1:
    raise newException(Defect, "Failed to open output pipe.")
  let pid = fork()
  if pid == -1:
    raise newException(Defect, "Failed to fork the fork process.")
  elif pid == 0:
    # child process
    let readfd = pipefd_in[0]
    discard close(pipefd_in[1]) # close write
    let writefd = pipefd_out[1]
    discard close(pipefd_out[0]) # close read
    discard dup2(readfd, stdin.getFileHandle())
    discard dup2(writefd, stdout.getFileHandle())
    discard close(pipefd_in[0])
    discard close(pipefd_out[1])
    runForkServer()
    assert false
  else:
    discard close(pipefd_in[0]) # close read
    discard close(pipefd_out[1]) # close write
    var readf, writef: File
    if not open(writef, pipefd_in[1], fmWrite):
      raise newException(Defect, "Failed to open output handle")
    if not open(readf, pipefd_out[0], fmRead):
      raise newException(Defect, "Failed to open input handle")
    result.ostream = newFileStream(writef)
    result.istream = newFileStream(readf)
pager.container.scrollRight() proc reshape(pager: Pager) {.jsfunc.} = pager.container.render() proc searchNext(pager: Pager) {.jsfunc.} = if pager.regex.issome: if not pager.reverseSearch: pager.container.cursorNextMatch(pager.regex.get, true) else: pager.container.cursorPrevMatch(pager.regex.get, true) proc searchPrev(pager: Pager) {.jsfunc.} = if pager.regex.issome: if not pager.reverseSearch: pager.container.cursorPrevMatch(pager.regex.get, true) else: pager.container.cursorNextMatch(pager.regex.get, true) proc statusMode(pager: Pager) = print(HVP(pager.attrs.height + 1, 1)) print(EL()) proc setLineEdit(pager: Pager, edit: LineEdit, mode: LineMode) = pager.statusMode() edit.writeStart() stdout.flushFile() pager.lineedit = some(edit) pager.linemode = mode proc clearLineEdit(pager: Pager) = pager.lineedit = none(LineEdit) proc searchForward(pager: Pager) {.jsfunc.} = pager.setLineEdit(readLine("/", pager.attrs.width, config = pager.config, tty = pager.tty), SEARCH_F) proc searchBackward(pager: Pager) {.jsfunc.} = pager.setLineEdit(readLine("?", pager.attrs.width, config = pager.config, tty = pager.tty), SEARCH_B) proc isearchForward(pager: Pager) {.jsfunc.} = pager.container.pushCursorPos() pager.setLineEdit(readLine("/", pager.attrs.width, config = pager.config, tty = pager.tty), ISEARCH_F) proc isearchBackward(pager: Pager) {.jsfunc.} = pager.container.pushCursorPos() pager.setLineEdit(readLine("?", pager.attrs.width, config = pager.config, tty = pager.tty), ISEARCH_B) proc newPager*(config: Config, attrs: TermAttributes, tty: File): Pager = new(result) result.config = config result.attrs = attrs result.tty = tty result.selector = newSelector[Container]() result.bwidth = attrs.width - 1 # writing to the last column is a bad idea it seems result.bheight = attrs.height - 1 result.display = newFixedGrid(result.bwidth, result.bheight) proc clearDisplay(pager: Pager) = pager.display = newFixedGrid(pager.bwidth, pager.bheight) proc refreshDisplay*(pager: Pager, container = pager.container) = var r: Rune var by = 0 pager.clearDisplay() for line in container.ilines(container.fromy ..< min(container.fromy + pager.bheight, container.numLines)): var w = 0 # width of the row so far var i = 0 # byte in line.str # Skip cells till buffer.fromx. while w < container.fromx and i < line.str.len: fastRuneAt(line.str, i, r) w += r.width() let dls = by * container.width # starting position of row in display # Fill in the gap in case we skipped more cells than fromx mandates (i.e. # we encountered a double-width character.) var k = 0 if w > container.fromx: while k < w - container.fromx: pager.display[dls + k].str &= ' ' inc k var cf = line.findFormat(w) var nf = line.findNextFormat(w) let startw = w # save this for later # Now fill in the visible part of the row. while i < line.str.len: let pw = w fastRuneAt(line.str, i, r) w += r.width() if w > container.fromx + pager.bwidth: break # die on exceeding the width limit if nf.pos != -1 and nf.pos <= pw: cf = nf nf = line.findNextFormat(pw) pager.display[dls + k].str &= r if cf.pos != -1: pager.display[dls + k].format = cf.format let tk = k + r.width() while k < tk and k < pager.bwidth - 1: inc k # Then, for each cell that has a mark, override its formatting with that # specified by the mark. #TODO honestly this was always broken anyways. not sure about how to re-implement it #var l = 0 #while l < pager.marks.len and buffer.marks[l].y < by: # inc l # linear search to find the first applicable mark #let aw = buffer.width - (startw - buffer.fromx) # actual width #while l < buffer.marks.len and buffer.marks[l].y == by: # let mark = buffer.marks[l] # inc l # if mark.x >= startw + aw or mark.x + mark.width < startw: continue # for i in max(mark.x, startw)..<min(mark.x + mark.width, startw + aw): # buffer.display[dls + i - startw].format = mark.format inc by func generateStatusMessage*(pager: Pager): string = var format = newFormat() var w = 0 for cell in pager.statusmsg: result &= format.processFormat(cell.format) result &= cell.str w += cell.width() if w < pager.bwidth: result &= EL() proc clearStatusMessage(pager: Pager) = pager.statusmsg = newFixedGrid(pager.bwidth) proc writeStatusMessage(pager: Pager, str: string, format: Format = Format()) = pager.clearStatusMessage() var i = 0 for r in str.runes: i += r.width() if i >= pager.statusmsg.len: pager.statusmsg[^1].str = "$" break pager.statusmsg[i].str &= r pager.statusmsg[i].format = format proc refreshStatusMsg*(pager: Pager) = let container = pager.container if container != nil: var msg = $(container.cursory + 1) & "/" & $container.numLines & " (" & $container.atPercentOf() & "%) " & "<" & container.getTitle() & ">" if container.hovertext.len > 0: msg &= " " & container.hovertext var format: Format format.reverse = true pager.writeStatusMessage(msg, format) func generateStatusOutput(pager: Pager): string = if pager.status.len > 0: result = pager.status[0] & EL() pager.status = pager.status[1..^1] else: return pager.generateStatusMessage() func generateFullOutput(pager: Pager): string = var x = 0 var w = 0 var format = newFormat() result &= HVP(1, 1) for cell in pager.display: if x >= pager.bwidth: result &= EL() result &= "\r\n" x = 0 w = 0 result &= format.processFormat(cell.format) result &= cell.str w += cell.width() inc x result &= EL() result &= "\r\n" proc displayCursor*(pager: Pager) = if pager.container == nil: return print(HVP(pager.container.acursory + 1, pager.container.acursorx + 1)) stdout.flushFile() proc displayStatus*(pager: Pager) = if pager.lineedit.isNone: pager.statusMode() print(pager.generateStatusOutput()) stdout.flushFile() proc displayPage*(pager: Pager) = stdout.hideCursor() print(SGR()) print(pager.generateFullOutput()) pager.displayStatus() pager.displayCursor() stdout.showCursor() if pager.lineedit.isSome: pager.statusMode() pager.lineedit.get.writePrompt() pager.lineedit.get.fullRedraw() stdout.flushFile() proc redraw(pager: Pager) {.jsfunc.} = pager.refreshDisplay() pager.refreshStatusMsg() pager.displayPage() proc addContainer*(pager: Pager, container: Container) = container.parent = pager.container if pager.container != nil: pager.container.children.add(container) pager.setContainer(container) assert int(container.ifd) != 0 pager.fdmap[container.ifd] = container pager.selector.registerHandle(int(container.ifd), {Read}, pager.container) proc dupeContainer(pager: Pager, container: Container, location: Option[URL]): Container = return container.dupeBuffer(pager.config, location) proc dupeBuffer*(pager: Pager, location = none(URL)) {.jsfunc.} = pager.addContainer(pager.dupeContainer(pager.container, location)) # The prevBuffer and nextBuffer procedures emulate w3m's PREV and NEXT # commands by traversing the container tree in a depth-first order. proc prevBuffer*(pager: Pager): bool {.jsfunc.} = if pager.container == nil: return false if pager.container.parent == nil: return false let n = pager.container.parent.children.find(pager.container) assert n != -1, "Container not a child of its parent" if n > 0: pager.setContainer(pager.container.parent.children[n - 1]) else: pager.setContainer(pager.container.parent) return true proc nextBuffer*(pager: Pager): bool {.jsfunc.} = if pager.container == nil: return false if pager.container.children.len > 0: pager.setContainer(pager.container.children[0]) return true if pager.container.parent == nil: return false let n = pager.container.parent.children.find(pager.container) assert n != -1, "Container not a child of its parent" if n < pager.container.parent.children.high: pager.setContainer(pager.container.parent.children[n + 1]) return true return false proc setStatusMessage*(pager: Pager, msg: string) = pager.status.add(msg) pager.refreshStatusMsg() proc lineInfo(pager: Pager) {.jsfunc.} = pager.setStatusMessage(pager.container.lineInfo()) proc deleteContainer(pager: Pager, container: Container) = if container.parent == nil and container.children.len == 0 and container != pager.container: return if container.parent != nil: let parent = container.parent let n = parent.children.find(container) assert n != -1, "Container not a child of its parent" for i in countdown(container.children.high, 0): let child = container.children[i] child.parent = container.parent parent.children.insert(child, n + 1) parent.children.delete(n) if container == pager.container: pager.setContainer(parent) elif container.children.len > 0: let parent = container.children[0] parent.parent = nil for i in 1..container.children.high: container.children[i].parent = parent parent.children.add(container.children[i]) if container == pager.container: pager.setContainer(parent) else: for child in container.children: child.parent = nil if container == pager.container: pager.setContainer(nil) container.parent = nil container.children.setLen(0) pager.fdmap.del(container.ifd) pager.selector.unregister(int(container.ifd)) container.istream.close() container.ostream.close() proc discardBuffer*(pager: Pager) {.jsfunc.} = if pager.container == nil or pager.container.parent == nil and pager.container.children.len == 0: pager.setStatusMessage("Cannot discard last buffer!") else: pager.deleteContainer(pager.container) proc toggleSource*(pager: Pager) {.jsfunc.} = if pager.container.sourcepair != nil: pager.setContainer(pager.container.sourcepair) else: let contenttype = if pager.container.contenttype.get("") == "text/html": some("text/plain") else: some("text/html") let container = pager.container.dupeBuffer(pager.config, contenttype = contenttype) container.sourcepair = pager.container pager.container.sourcepair = container pager.container.children.add(container) # Load request in a new buffer. proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(string), replace: Container = nil) = if prevurl.isnone or not prevurl.get.equals(request.url, true) or request.url.hash == "" or request.httpmethod != HTTP_GET: # Basically, we want to reload the page *only* when # a) we force a reload (by setting prevurl to none) # b) or the new URL isn't just the old URL + an anchor # I think this makes navigation pretty natural, or at least very close to # what other browsers do. Still, it would be nice if we got some visual # feedback on what is actually going to happen when typing a URL; TODO. let source = BufferSource( t: LOAD_REQUEST, request: request, contenttype: ctype, location: request.url ) let container = newBuffer(pager.config, source, pager.tty.getFileHandle()) container.replace = replace pager.addContainer(container) container.load() else: pager.container.redirect = some(request.url) pager.container.gotoAnchor(request.url.anchor) # When the user has passed a partial URL as an argument, they might've meant # either: # * file://$PWD/<file> # * https://<url> # So we attempt to load both, and see what works. # (TODO: make this optional) proc loadURL*(pager: Pager, url: string, ctype = none(string)) = let firstparse = parseURL(url) if firstparse.issome: let prev = if pager.container != nil: some(pager.container.source.location) else: none(URL) pager.gotoURL(newRequest(firstparse.get), prev, ctype) return var urls: seq[URL] let pageurl = parseURL("https://" & url) if pageurl.isSome: # attempt to load remote page urls.add(pageurl.get) let cdir = parseURL("file://" & getCurrentDir() & DirSep) let purl = percentEncode(url, LocalPathPercentEncodeSet) if purl != url: let newurl = parseURL(purl, cdir) if newurl.isSome: urls.add(newurl.get) let localurl = parseURL(url, cdir) if localurl.isSome: # attempt to load local file urls.add(localurl.get) if urls.len == 0: pager.setStatusMessage("Invalid URL " & url) else: let prevc = pager.container pager.gotoURL(newRequest(urls.pop()), ctype = ctype) if pager.container != prevc: pager.container.retry = urls proc readPipe*(pager: Pager, ctype: Option[string]) = let source = BufferSource( t: LOAD_PIPE, fd: stdin.getFileHandle(), contenttype: some(ctype.get("text/plain")), location: parseUrl("file://-").get ) let container = newBuffer(pager.config, source, pager.tty.getFileHandle(), ispipe = true) pager.addContainer(container) container.load() proc updateReadLineISearch(pager: Pager, linemode: LineMode) = let lineedit = pager.lineedit.get case lineedit.state of CANCEL: pager.iregex = none(Regex) pager.container.popCursorPos() of EDIT: let x = $lineedit.news if x != "": pager.iregex = compileSearchRegex(x) pager.container.popCursorPos() if pager.iregex.isSome: if linemode == ISEARCH_F: pager.container.cursorNextMatch(pager.iregex.get, true) else: pager.container.cursorPrevMatch(pager.iregex.get, true) pager.container.pushCursorPos() pager.displayPage() pager.statusMode() pager.lineedit.get.fullRedraw() of FINISH: if pager.iregex.isSome: pager.regex = pager.iregex pager.reverseSearch = linemode == ISEARCH_B proc updateReadLine*(pager: Pager) = let lineedit = pager.lineedit.get template s: string = $lineedit.news if pager.linemode in {ISEARCH_F, ISEARCH_B}: pager.updateReadLineISearch(pager.linemode) else: case lineedit.state of EDIT: return of FINISH: case pager.linemode of LOCATION: pager.loadURL(s) of USERNAME: pager.username = s pager.setLineEdit(readLine("Password: ", pager.attrs.width, hide = true, config = pager.config, tty = pager.tty), PASSWORD) of PASSWORD: let url = newURL(pager.container.source.location) url.username = pager.username url.password = s pager.username = "" pager.gotoURL(newRequest(url), some(pager.container.source.location), replace = pager.container) of COMMAND: pager.command = s of BUFFER: pager.container.readSuccess(s) of SEARCH_F: let x = s if x != "": pager.regex = compileSearchRegex(x) pager.reverseSearch = false pager.searchNext() of SEARCH_B: let x = s if x != "": pager.regex = compileSearchRegex(x) pager.reverseSearch = true pager.searchPrev() else: discard of CANCEL: case pager.linemode of USERNAME: pager.discardBuffer() of PASSWORD: pager.username = "" pager.discardBuffer() of BUFFER: pager.container.readCanceled() else: discard if lineedit.state in {CANCEL, FINISH}: if pager.lineedit.get == lineedit: pager.clearLineEdit() print('\r') print(EL()) pager.displayPage() # Open a URL prompt and visit the specified URL. proc changeLocation(pager: Pager) {.jsfunc.} = var url = pager.container.source.location.serialize() pager.setLineEdit(readLine("URL: ", pager.attrs.width, current = url, config = pager.config, tty = pager.tty), LOCATION) # Reload the page in a new buffer, then kill the previous buffer. proc reloadPage(pager: Pager) {.jsfunc.} = pager.gotoURL(newRequest(pager.container.source.location), none(URL), pager.container.contenttype, pager.container) proc click(pager: Pager) {.jsfunc.} = pager.container.click() proc authorize*(pager: Pager) = pager.setLineEdit(readLine("Username: ", pager.attrs.width, config = pager.config, tty = pager.tty), USERNAME) proc handleEvent*(pager: Pager, container: Container): bool = let event = container.handleEvent() case event.t of FAIL: pager.deleteContainer(container) if container.retry.len > 0: pager.gotoURL(newRequest(container.retry.pop()), ctype = container.contenttype) else: pager.setStatusMessage("Couldn't load " & $container.source.location & " (error code " & $container.code & ")") pager.displayStatus() pager.displayCursor() if pager.container == nil: return false of SUCCESS: container.render() if container.replace != nil: container.children.add(container.replace.children) for child in container.children: child.parent = container container.replace.children.setLen(0) if container.replace.parent != nil: container.parent = container.replace.parent let n = container.replace.parent.children.find(container.replace) assert n != -1, "Container not a child of its parent" container.parent.children[n] = container if pager.container == container.replace: pager.setContainer(container) of NEEDS_AUTH: if pager.container == container: pager.authorize() of REDIRECT: let redirect = container.redirect.get pager.setStatusMessage("Redirecting to " & $redirect) pager.displayStatus() pager.displayCursor() pager.gotoURL(newRequest(redirect), some(pager.container.source.location), replace = pager.container) of ANCHOR: pager.addContainer(pager.dupeContainer(container, container.redirect)) of NO_ANCHOR: pager.setStatusMessage("Couldn't find anchor " & container.redirect.get.anchor) pager.displayStatus() pager.displayCursor() of UPDATE: if container == pager.container: pager.refreshDisplay() pager.refreshStatusMsg() pager.displayPage() of JUMP: if container == pager.container: pager.refreshStatusMsg() pager.displayStatus() pager.displayCursor() of STATUS: if container == pager.container: pager.refreshStatusMsg() pager.displayStatus() pager.displayCursor() of READ_LINE: if container == pager.container: pager.setLineEdit(readLine(event.prompt, pager.bwidth, current = event.value, hide = event.password, config = pager.config, tty = pager.tty), BUFFER) of OPEN: pager.gotoURL(event.request, some(container.source.location)) of NO_EVENT: discard return true proc addPagerModule*(ctx: JSContext) = ctx.registerType(Pager)