-- vim: ft=terra
local util = dofile('common.lua')
local buildopts, buildargs = util.parseargs{...}
config = dofile('config.lua')
lib = {
init = {}, util = util;
load = function(lst)
for _, l in pairs(lst) do
io.stdout:write(' · processing module \27[1m' .. l ..'\27[m… ')
local path = {}
for m in l:gmatch('([^:]+)') do path[#path+1]=m end
local tgt = lib
for i=1,#path-1 do
if tgt[path[i]] == nil then tgt[path[i]] = {} end
tgt = tgt[path[i]]
end
local chunk = terralib.loadfile(l:gsub(':','/') .. '.t')
if chunk ~= nil then
tgt[path[#path]:gsub('-','_')] = chunk()
print(' \27[1m[ \27[32mok\27[;1m ]\27[m')
else
print(' \27[1m[\27[31mfail\27[;1m]\27[m')
os.exit(2)
end
end
end;
loadlib = function(name,hdr)
local p = config.pkg[name]
-- for _,v in pairs(p.dylibs) do
-- terralib.linklibrary(p.libdir .. '/' .. v)
-- end
return terralib.includec(p.incdir .. '/' .. hdr)
end;
dispatch = function(tbl)
return macro(function(v,...)
for ty,fn in pairs(tbl) do
if v.tree.type == ty then return fn(v,...) end
end
return (tbl[false])(v,...)
end)
end;
emit_unitary = function(nl,fd,...)
local code = {}
local defs = {}
for i,v in ipairs{...} do
local str, ct
if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then
if v.tree.type.convertible == 'tuple' then
str = `v._0
ct = `v._1
else
local n = symbol(v.tree.type)
defs[#defs + 1] = quote var [n] = v end
str = n
ct = `lib.str.sz(n)
end
else
if type(v) == 'string' or type(v) == 'number' then
str = tostring(v)
else--if v.tree:is 'constant' then
str = tostring(v:asvalue())
end
ct = ct or #str
end
code[#code+1] = `lib.io.send(fd, str, ct)
end
if nl == true then code[#code+1] = `lib.io.send(fd, '\n', 1)
elseif nl then code[#code+1] = `lib.io.send(fd, nl, [#nl]) end
return quote [defs] in [code] end
end;
emitv = function(nl,fd,...)
local vec = {}
local defs = {}
for i,v in ipairs{...} do
local str, ct
if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then
if v.tree.type.convertible == 'tuple' then
str = `v._0
ct = `v._1
else
local n = symbol(v.tree.type)
defs[#defs + 1] = quote var [n] = v end
str = n
ct = `lib.str.sz(n)
end
else
if type(v) == 'string' or type(v) == 'number' then
str = tostring(v)
else--if v.tree:is 'constant' then
str = tostring(v:asvalue())
end
ct = ct or #str
end
vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](str), iov_len = ct}
end
if nl == true then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1}
elseif nl then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](nl), iov_len = [#nl]} end
return quote
[defs]
var strs = array( [vec] )
in lib.uio.writev(fd, strs, [#vec]) end
end;
trn = macro(function(cond, i, e)
return quote
var c: bool = [cond]
var r: i.tree.type
if c == true then r = i else r = e end
in r end
end);
typeof = macro(function(exp) return exp.tree.type end);
coalesce = macro(function(...)
local args = {...}
local ty = args[1].tree.type
local val = symbol(ty)
local empty
if ty.ptr_basetype then empty = `[ty]{ptr=nil,ct=0}
elseif ty.type == 'integer' then empty = `0
else empty = `nil end
local exp = quote val = [empty] end
for i=#args, 1, -1 do
local v = args[i]
if ty.ptr_basetype then
exp = quote
if [v].ptr ~= nil
then val = v
else [exp]
end
end
else
exp = quote
if [v] ~= [empty]
then val = v
else [exp]
end
end
end
end
local q = quote
var [val]
[exp]
in val end
return q
end);
proc = {
fork = terralib.externfunction('fork', {} -> int);
exit = terralib.externfunction('exit', int -> {});
getenv = terralib.externfunction('getenv', rawstring -> rawstring);
exec = terralib.externfunction('execv', {rawstring,&rawstring} -> int);
execp = terralib.externfunction('execvp', {rawstring,&rawstring} -> int);
};
io = {
send = terralib.externfunction('write', {int, rawstring, intptr} -> ptrdiff);
recv = terralib.externfunction('read', {int, rawstring, intptr} -> ptrdiff);
close = terralib.externfunction('close', {int} -> int);
say = macro(function(msg) return `lib.io.send(2, msg, [#(msg:asvalue())]) end);
fmt = terralib.externfunction('printf',
terralib.types.funcpointer({rawstring},{int},true));
ttyp = terralib.externfunction('isatty', int -> int);
};
str = { sz = terralib.externfunction('strlen', rawstring -> intptr) };
copy = function(tbl)
local new = {}
for k,v in pairs(tbl) do new[k] = v end
setmetatable(new, getmetatable(tbl))
return new
end;
osclock = terralib.includec 'time.h';
}
if config.posix then
lib.uio = terralib.includec 'sys/uio.h';
lib.emit = lib.emitv -- use more efficient call where available
else lib.emit = lib.emit_unitary end
lib.noise = {
level = global(uint8,1);
starttime = global(lib.osclock.time_t);
lasttime = global(lib.osclock.time_t);
header = function(code,txt,mod)
if mod then
return string.format('\27[%s;1m(%s %s)\27[m ', code,mod,txt)
else
return string.format('\27[%s;1m(%s)\27[m ', code,txt)
end
end;
}
local terra timehdr()
var now = lib.osclock.time(nil)
var diff = now - lib.noise.lasttime
if diff > 30 then -- print cur time
lib.noise.lasttime = now
var curtime: int8[26]
lib.osclock.ctime_r(&now, &curtime[0])
for i=0,26 do if curtime[i] == @'\n' then curtime[i] = 0 break end end -- :/
[ lib.emit(false, 2, '\27[1m', `&curtime[0], '\27[;36m\n +00 ') ]
else -- print time since last msg
var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0)
[ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ]
end
end
local defrep = function(level,n,code)
if level >= 3 and config.debug == false then
return macro(function(...) return {} end)
end
return macro(function(...)
local fn = (...).filename
local ln = tostring((...).linenumber)
local dbgtag = string.format('\27[35m · \27[34m%s:\27[1m%s\27[m\n', fn,ln)
local q = lib.emit(level < 3 and true or dbgtag, 2, lib.noise.header(code,n), ...)
return quote
--lib.io.fmt(['attempting to emit at ' .. fn..':'..ln.. '\n'])
if lib.noise.level >= level then timehdr(); [q] end end
end);
end
local terra final_cleanup :: {} -> {}
lib.dbg = defrep(3,'debug', '32')
lib.report = defrep(2,'info', '35')
lib.warn = defrep(1,'warn', '33')
lib.bail = macro(function(...)
local q = lib.emit(true, 2, lib.noise.header('31','fatal'), ...)
return quote
if lib.noise.level ~= 0 then
timehdr(); [q]
end
final_cleanup()
lib.proc.exit(1)
end
end);
lib.stat = terralib.memoize(function(ty)
local n = struct {
ok: bool
union {
error: uint8
val: ty
}
}
n.name = string.format("stat<%s>", ty.name)
n.stat_basetype = ty
return n
end)
lib.enum = function(tbl)
if type(tbl) == 'string' then -- shorthand syntax
local t = {}
for w in tbl:gmatch('(%g+)') do t[#t+1] = w end
tbl = t
end
local ty = uint8
if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
elseif #tbl >= 2^16 then ty = uint32
elseif #tbl >= 2^8 then ty = uint16 end
local o = { t = ty, members = tbl }
local strings = {}
for i, name in ipairs(tbl) do
o[name] = `[ty]([i - 1])
strings[i] = `[lib.str.t]{ptr=[name], ct=[#name]}
end
o._str = terra(val: ty)
var l = array([strings])
return l[val]
end
return o
end
lib.set = function(tbl)
local bytes = math.ceil(#tbl / 8)
local o = {}
for i, name in ipairs(tbl) do o[name] = i end
local struct set { _store: uint8[bytes] }
local struct bit { _v: intptr _set: &set}
terra set:clear() for i=0,bytes do self._store[i] = 0 end end
terra set:fill() for i=0,bytes do self._store[i] = 0xFF end end
set.members = tbl
set.idvmap = o
set.null = quote var s: set s:clear() in s end
set.name = string.format('set<%s>', table.concat(tbl, '|'))
set.metamethods.__entrymissing = macro(function(val, obj)
if o[val] == nil then error('value ' .. val .. ' not in set') end
return `bit { _v=[o[val] - 1], _set = &(obj) }
end)
terra set:any()
for i = 0, bytes - 1 do
if self._store[i] ~= 0 then return true end
end
return false
end
terra set:sz()
var ct: intptr = 0
--for i = 0, [math.floor(#tbl/8)] do
-- ct = ct + lib.math.ll.ctpop_u8(self._store[i])
--end
--[(function()
-- if #tbl % 8 ~= 0 then
-- local last = #tbl-1
-- local msk = (2 ^ (#tbl % 8)) - 1
-- return quote ct = ct + lib.math.ll.ctpop_u8(self._store[last] and [uint8](msk)) end
-- else return {} end
--end)()]
for i = 0, [#tbl] do
if (self._store[i/8] and (1 << i % 8)) ~= 0 then ct = ct + 1 end
end
return ct
end
set.methods.dump = macro(function(self)
local q = quote lib.io.say('dumping set:\n') end
for i,v in ipairs(tbl) do
q = quote [q]
if [bool](self.[v])
then lib.io.say([' - ' .. v .. ': true\n'])
else lib.io.say([' - ' .. v .. ': false\n'])
end
end
end
return q
end)
terra set:setbit(i: intptr, val: bool)
if val then
self._store[i/8] = self._store[i/8] or (1 << (i % 8))
else
self._store[i/8] = self._store[i/8] and not (1 << (i % 8))
end
end
local qksetexp = function(self,idx,rhs)
local ch,bit
if terralib.isconstant(idx) then
ch = math.floor(idx/8)
bit = idx%8
else
ch = `[idx]/8
bit = `[idx]%8
end
if terralib.isconstant(rhs) then
local b = rhs:asvalue()
if b == true then return quote
self._store[ch] = self._store[ch] or (1 << bit)
end elseif b == false then return quote
self._store[ch] = self._store[ch] and not (1 << bit)
end end
else
return quote self:setbit([ch], rhs) end
end
end
set.metamethods.__update = macro(qksetexp)
set.metamethods.__setentry = macro(function(field,self,rhs)
return `self:setbit([o[field]-1],rhs)
--return qksetexp(self, o[field], rhs)
end)
set.bits = {}
for i,v in ipairs(tbl) do
set.bits[v] = quote var b: set b:clear() b:setbit([i-1], true) in b end
end
set.metamethods.__add = macro(function(self,other)
local new = symbol(set)
local q = quote var [new] new:clear() end
for i = 0, bytes - 1 do
q = quote [q]
new._store[i] = self._store[i] or other._store[i]
end
end
return quote [q] in new end
end)
set.metamethods.__and = macro(function(self,other)
local new = symbol(set)
local q = quote var [new] new:clear() end
for i = 0, bytes - 1 do
q = quote [q]
new._store[i] = self._store[i] and other._store[i]
end
end
return quote [q] in new end
end)
set.metamethods.__eq = macro(function(self,other)
local rt = symbol(bool)
local fb if #tbl % 8 == 0 then fb = bytes - 1 else fb = bytes - 2 end
local q = quote rt = true end
for i = 0, fb do
q = quote
if self._store[i] ~= other._store[i] then rt = false else [q] end
end
end
-- we need to mask out any extraneous bits the values might have, as we
-- don't want the kind of noise introduced by :fill() to affect comparison
if #tbl % 8 ~= 0 then
local last = #tbl-1
local msk = (2 ^ (#tbl % 8)) - 1
q = quote
if (self._store [last] and [uint8](msk)) ~=
(other._store[last] and [uint8](msk)) then rt = false else [q] end
end
end
return quote var [rt]; [q] in rt end
end)
set.metamethods.__not = macro(function(self)
local new = symbol(set)
local q = quote var [new] new:clear() end
for i = 0, bytes - 1 do
q = quote [q]
new._store[i] = not self._store[i]
end
end
return quote [q] in new end
end)
set.metamethods.__sub = macro(function(self,other)
local new = symbol(set)
local q = quote var [new] new:clear() end
for i = 0, bytes - 1 do
q = quote [q]
new._store[i] = self._store[i] and (not other._store[i])
end
end
return quote [q] in new end
end)
bit.metamethods.__cast = function(from,to,e)
local q = quote var s = e
in (s._set._store[s._v/8] and (1 << s._v % 8)) end
if to == bit then error('casting to bit is not meaningful')
elseif to == bool then return `([q] ~= 0)
elseif to:isintegral() then return q
elseif from == bit then error('cannot cast bit to ' .. tostring(to))
else return nil end
end
bit.metamethods.__apply = terra(self: &bit): bool return @self end
bit.metamethods.__lshift = terra(self: &bit, hl: bool)
var byte = self._v / 8
var bit = self._v % 8
if hl then
self._set._store[byte] = self._set._store[byte] or (1 << bit)
else
self._set._store[byte] = self._set._store[byte] and not (1 << bit)
end
end
return set
end
lib.err = lib.loadlib('mbedtls','mbedtls/error.h')
lib.rsa = lib.loadlib('mbedtls','mbedtls/rsa.h')
lib.asn1 = lib.loadlib('mbedtls','mbedtls/asn1.h')
-- lib.pk = lib.loadlib('mbedtls','mbedtls/pk.h')
--sigh
lib.pk = terralib.includecstring [[
#include <mbedtls/pk.h>
mbedtls_rsa_context* pk2rsa(const mbedtls_pk_context pk) {
return mbedtls_pk_rsa(pk);
}
]]
lib.md = lib.loadlib('mbedtls','mbedtls/md.h')
lib.drbg = lib.loadlib('mbedtls','mbedtls/ctr_drbg.h')
lib.entropy = lib.loadlib('mbedtls','mbedtls/entropy.h')
lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h')
lib.net = lib.loadlib('mongoose','mongoose.h')
lib.pq = lib.loadlib('libpq','libpq-fe.h')
lib.jc = lib.loadlib('mjson','mjson.h')
lib.load {
'mem', 'math', 'str', 'file', 'crypt', 'ipc';
'http', 'html', 'session', 'tpl', 'store', 'acl';
'mime'; -- mimetype database & whitelist
'smackdown'; -- md-alike parser
'munge'; -- miscellaneous conversion/munging functions
}
local be = {}
for _, b in pairs(config.backends) do
be[#be+1] = terralib.loadfile(string.format('backend/%s.t',b))()
end
lib.store.backends = global(`array([be]))
lib.cmdparse = terralib.loadfile('cmdparse.t')()
do local collate = function(path,f, ...)
return loadfile(path..'/'..f..'.lua')(path, ...)
end
data = {
doc = collate('doc','load');
view = collate('view','load');
static = {};
stmap = global(lib.mem.ref(int8)[#config.embeds]); -- array of pointers to static content
} end
for i,e in ipairs(config.embeds) do local v = e[1]
local fh = io.open('static/' .. v,'r')
if fh == nil then error('static file ' .. v .. ' missing') end
data.static[v] = fh:read('*a') fh:close()
end
-- slightly silly -- because we're using plain lua to gather up
-- the template sources, they have to be actually turned into
-- templates in the terra code, so we "mutate" them here
for k,v in pairs(data.view) do
local t = lib.tpl.mk { body = v, id = 'view/'..k }
data.view[k] = t
end
lib.load {
'srv';
'render:nav';
'render:nym';
'render:login';
'render:profile';
'render:compose';
'render:tweet';
'render:tweet-page';
'render:user-page';
'render:timeline';
'render:notices';
'render:media-gallery';
'render:docpage';
'render:conf:profile';
'render:conf:circles';
'render:conf:sec';
'render:conf:users';
'render:conf:avi';
'render:conf';
'api:lp:actor';
'api:lp:tweet';
'api:lp:outbox';
'api:webfinger';
'route';
}
do
local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
terra version() lib.io.send(1, p, [#p]) end
end
terra lib.noise.init(default_level: uint)
lib.noise.starttime = lib.osclock.time(nil)
lib.noise.lasttime = 0
var n = lib.proc.getenv('parsav_noise')
if n ~= nil then
if n[0] >= 0x30 and n[0] <= 0x39 and n[1] == 0 then
lib.noise.level = n[0] - 0x30
return
end
end
lib.noise.level = default_level
end
lib.load{'mgtool'}
local options = lib.cmdparse {
version = {'V', 'display information about the binary build and exit'};
verbose = {'v', 'increase logging verbosity', inc=1};
quiet = {'q', 'do not print to standard out'};
help = {'h', 'display this list'};
backend_file = {'B', 'init from specified backend file', consume=1};
static_dir = {'S', 'directory with overrides for static content', consume=1};
builtin_data = {'D', 'do not load static content overrides at runtime under any circumstances'};
instance = {'i', 'set an instance name to make it easier to control multiple daemons', consume = 1};
no_ipc = {'I', 'disable IPC'};
chroot = {'C', 'chroot to the specified directory after starting and connecting to backends', consume=1};
no_lockdown = {'L', 'don\'t drop privileges or apply other security measures that could cause compatibility or communication problems'}
}
local static_setup = quote end
local mapin = quote end
local odir = symbol(rawstring)
local pathbuf = symbol(lib.str.acc)
for i,e in ipairs(config.embeds) do local v = e[1]
local d = data.static[v]
static_setup = quote [static_setup]
([data.stmap])[([i-1])] = ([lib.mem.ref(int8)] { ptr = [d], ct = [#d] })
end
mapin = quote [mapin]
var osz = pathbuf.sz
pathbuf:push([v],[#v])
var f = lib.file.open(pathbuf.buf, [lib.file.mode.read])
if f.ok then defer f.val:close()
var map = f.val:mapin(0,0) -- currently maps are leaked, maybe do something more elegant in future
lib.report('loading static override content from ', pathbuf.buf)
([data.stmap])[([i-1])] = ([lib.mem.ref(int8)] {
ptr = [rawstring](map.addr);
ct = map.sz;
})
end
pathbuf.sz = osz
end
end
local terra static_init(mode: &options)
[static_setup]
if mode.builtin_data then return end
var [odir] = lib.proc.getenv('parsav_override_dir')
if mode.static_dir ~= nil then
odir=@mode.static_dir
end
if odir == nil then [
config.prefix_static and quote
odir = [config.prefix_static]
end or quote return end
] end
var [pathbuf] defer pathbuf:free()
pathbuf:compose(odir,'/')
[mapin]
end
terra final_cleanup()
lib.ipc.global_emperor:release()
end
local terra entry_daemon(argc: int, argv: &rawstring): int
if argc < 1 then lib.bail('bad invocation!') end
lib.noise.init(1)
[lib.init]
-- shut mongoose the fuck up
lib.net.mg_log_set_callback([terra(msg: &opaque, sz: int, u: &opaque) end], nil)
var srv: lib.srv.overlord
do var mode: options
mode:parse(argc,argv) defer mode:free()
static_init(&mode)
if mode.version then version() return 0 end
if mode.help then
[ lib.emit(true, 1, 'usage: ',`argv[0],' ', options.helptxt.flags, ' [<args>…]', options.helptxt.opts) ]
return 0
end
if mode.quiet then lib.noise.level = 0 end
var cnf: rawstring
if mode.backend_file ~= nil
then cnf = @mode.backend_file
else cnf = lib.proc.getenv('parsav_backend_file')
end
if cnf == nil then cnf = [config.prefix_conf .. "/backend.conf"] end
srv:setup(cnf)
if argv[0][0] == @'-' and argv[0][1] == 0 then
lib.ipc.notify_parent(lib.ipc.signals.state_success)
argv[0] = 'parsav service daemon'
end
srv:start(lib.trn(mode.instance ~= nil, @mode.instance, nil))
end
lib.ipc.global_emperor = lib.ipc.emperor.mk(true) defer lib.ipc.global_emperor:release()
lib.report('listening for requests')
while lib.ipc.proc_active do
srv:poll()
while true do
var d: lib.ipc.demand
lib.ipc.global_emperor:poll(&d)
var a = lib.ipc.ack {success = true}
if d.cmd == lib.ipc.cmd.none then break
elseif d.cmd == lib.ipc.cmd.stop then
lib.ipc.proc_active = false
elseif d.cmd == lib.ipc.cmd.enumerate then
if srv.id ~= nil then
lib.str.ncpy(&a.iname[0], srv.id, [(`a.iname).tree.type.N])
else a.iname[0] = 0 end
elseif d.cmd == lib.ipc.cmd.chnoise then
lib.noise.level = d.operand
elseif d.cmd == lib.ipc.cmd.cfgrefresh then
srv.cfg:purge()
srv.cfg:load()
end
d:ack(&lib.ipc.global_emperor, &a)
end
end
srv:shutdown()
return 0
end
local bflag = function(long,short)
if short and util.has(buildopts, short) then return true end
if long and util.has(buildopts, long) then return true end
return false
end
if bflag('dump-config','C') then
print(util.dump(config))
os.exit(0)
end
local holler = print
local suffix = config.exe and '' or ('.'..config.outform)
local out = 'parsavd' .. suffix
local linkargs = {'-g'}
local target = config.tgttrip and terralib.newtarget {
Triple = config.tgttrip;
CPU = config.tgtcpu;
FloatABIHard = config.tgthf;
} or nil
if bflag('quiet','q') then holler = function() end end
if target then holler(' * building for triple ' .. tostring(target)) end
if bflag('asan','s') then linkargs[#linkargs+1] = '-fsanitize=address' end
if bflag('lsan','S') then linkargs[#linkargs+1] = '-fsanitize=leak' end
if config.exe then
for _,p in pairs(config.pkg) do util.append(linkargs, p.linkargs) end
end
local linkargs_d = linkargs -- controller is not multithreaded
if config.posix then
linkargs_d[#linkargs_d+1] = '-pthread'
end
holler(' → writing \27[1mparsav\27[m with "' .. table.concat(linkargs,' ') .. '"')
terralib.saveobj('parsav' ..suffix, { main = lib.mgtool }, linkargs, target)
holler(' → writing \27[1mparsavd\27[m with "' .. table.concat(linkargs_d,' ') .. '"')
terralib.saveobj('parsavd'..suffix, { main = entry_daemon }, linkargs_d, target)