-- lib/supple/comms.lua
--
-- Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine
--
-- Management of communications between host and sandbox
--
-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
--
-- For licence terms, see COPYING
--

local luxio = require "luxio"

local capi = require "supple.capi"
local request = require "supple.request"
local objects = require "supple.objects"
local track = require "supple.track"

local unpack = unpack
local tonumber = tonumber
local error = error
local getinfo = debug.getinfo
local concat = table.concat
local xpcall = xpcall
local gc = collectgarbage

local fd = -1

local function set_fd(_fd)
   fd = _fd
end

local function send_msg(msg)
   if (#msg > 99999) then
      error("Message too long")
   end
   local msglen = ("%05d"):format(#msg)
   track.record("XMIT", msg)
   luxio.write(fd, msglen .. msg)
end

local function recv_msg()
   local len, errno = luxio.read(fd, 5)
   if type(len) ~= "string" then
      error(luxio.strerror(errno))
   end
   if #len < 5 then
      error("Unable to read 5 byte length")
   end
   len = tonumber(len)
   if len == nil or len < 1 or len > 99999 then
      error("Odd, len didn't translate properly")
   end
   local str = luxio.read(fd, len)
   if type(str) ~= "string" or #str ~= len then
      error("Unable to read " .. tostring(len) .. " bytes of msg")
   end
   track.record("RECV", str)
   return str
end

local function captcha(msg)
   local traceback = {}
   local level = 2
   local info = getinfo(level, "Snlf")
   local function in_supple()
      return info.short_src:match("/supple/[^%.]+%.lua$")
   end
   while info and info.func ~= xpcall do
--      if info.currentline > 0 and not in_supple() then
      if true then
	 local ttype, tag = objects.find_tag(info.func)
	 if ttype then
	    info.name = tag
	 end
	 local line = 
	    ("\t%s:%d in function %s"):format(info.short_src,
					      info.currentline,
					      info.name or "<anon>")
	 traceback[#traceback+1] = line
      end
      level = level + 1
      info = getinfo(level, "Snlf")
   end
   return {msg, (concat(traceback, "\n") or "") .. "\nComms track:\n" .. track.stop()}
end

local limits = {}
math.randomseed(os.time())
math.randomseed(math.random(luxio.getpid()))
local count_per_hook = 10 + (math.random(5) - 3)
local limiting = false

local function limit_hook()
   local inside = getinfo(2, "Snlf")
   if inside.short_src == "[C]" or inside.name == "(tail call)" then
      return
   end
   if limiting and not inside.short_src:match("/supple/[^%.]+%.lua$") then
      if limits.count then
	 limits.count = limits.count - count_per_hook
	 if limits.count < 0 then
	    limiting = false
	    error("Used too many instructions", 2)
	 end
      end
      if limits.memory then
	 if limits.memory < gc "count" then
	    limiting = false
	    error("Exceeded memory usage limit", 2)
	 end
      end
   end
end

local function wait_for_response()
   repeat
      -- We *MUST* disable GC for the duration of receiving and parsing the
      -- message, lest we send a __gc transaction between the first and second
      -- read operations.  Or indeed if we GC and object between receiving the
      -- message and deserialising and thus anchoring the object. If we error
      -- out here, we don't really care about restarting gc since we won't
      -- survive long anyhow.
      gc "stop"
      -- Also anchor all the objects just in case
      local anchor = objects.get_object_anchor()
      -- Now receive and deserialise (thus anchoring relevant objects)
      local back = request.deserialise(recv_msg())
      -- Now that the request is anchored, release the extra anchor and
      -- re-allow GC
      anchor = nil
      gc "restart"
      -- And get on with parsing the message we received..

      -- back could be three things
      -- an error (raise it)
      if back.error then
	 local msg = back.message
	 if back.traceback and back.traceback ~= "" then
	    msg = msg .. "\n" .. back.traceback
	 end
	 error(msg, 2)
      end
      -- A result, return it
      if back.error == false then
	 return unpack(back.results)
      end
      -- A method call, call it
      
      local function safe_method(fn)
	 local ok, res = xpcall(fn, captcha)
	 local resp
	 if not ok then
	    resp = request.error(unpack(res))
	 else
	    resp = request.response(unpack(res))
	 end
	 -- Force anything no longer anchored to be GC'd before we reply
	 gc "collect"
	 send_msg(resp)
      end
      if back.method == "__gc" then
	 -- __gc is the garbage collect mechanism
	 objects.forget_mine(back.object)
	 -- Force anything no longer anchored to be GC'd before we reply
	 gc "collect"
	 send_msg(request.response())
      elseif back.method == "__call" then
	 -- __call is the function call mechanism
	 safe_method(function()
			local obj = objects.receive { tag = back.object }
			return {obj(unpack(back.args))}
		     end)
      elseif back.method == "__len" then
	 safe_method(function()
			local obj = objects.receive { tag = back.object }
			return {#obj}
		     end)
      elseif back.method == "__index" then
	 safe_method(function()
			local obj = objects.receive { tag = back.object }
			return {obj[back.args[1]]}
		     end)
      elseif back.method == "__newindex" then
	 safe_method(function()
			local obj = objects.receive { tag = back.object }
			obj[back.args[1]] = back.args[2]
			return {}
		     end)
      elseif back.method == "__next" then
	 safe_method(function()
			local obj = objects.receive { tag = back.object }
			return {next(obj, back.args[1])}
		     end)
      else
	 safe_method(function()
			local obj = objects.receive { tag = back.object }
			local meth = capi.raw_getmm(obj, back.method)
			if not meth then
			   error("Unknown or disallowed method: " .. back.method, "")
			end
			return {meth(obj, unpack(back.args))}
		     end)
      end
   until false
end

local function make_call(object, method, ...)
   gc "stop"
   track.enter("make_call", object, method)
   local req = request.request(object, method, ...)
   send_msg(req)
   local ret = {wait_for_response()}
   track.leave("make_call", object, method)
   gc "restart"
   return unpack(ret)
end

local function set_limits(newlimits)
   limits = newlimits
   debug.sethook()
   debug.sethook(limit_hook, "c", count_per_hook)
   limiting = true
end

return {
   call = make_call,
   _wait = wait_for_response,
   _set_fd = set_fd,
   set_limits = set_limits,
}
