200 lines
5.3 KiB
Lua
200 lines
5.3 KiB
Lua
---@class ExecuteResult
|
|
---@field stdout string
|
|
---@field code integer
|
|
---@field time_ms number
|
|
---@field tled boolean
|
|
---@field mled boolean
|
|
---@field peak_mb number
|
|
---@field signal string|nil
|
|
|
|
local M = {}
|
|
local constants = require('cp.constants')
|
|
local logger = require('cp.log')
|
|
local utils = require('cp.utils')
|
|
|
|
local filetype_to_language = constants.filetype_to_language
|
|
|
|
local function get_language_from_file(source_file, contest_config)
|
|
local ext = vim.fn.fnamemodify(source_file, ':e')
|
|
return filetype_to_language[ext] or contest_config.default_language
|
|
end
|
|
|
|
local function substitute_template(cmd_template, substitutions)
|
|
local out = {}
|
|
for _, a in ipairs(cmd_template) do
|
|
local s = a
|
|
for k, v in pairs(substitutions) do
|
|
s = s:gsub('{' .. k .. '}', v)
|
|
end
|
|
table.insert(out, s)
|
|
end
|
|
return out
|
|
end
|
|
|
|
function M.build_command(cmd_template, executable, substitutions)
|
|
local cmd = substitute_template(cmd_template, substitutions)
|
|
if executable then
|
|
table.insert(cmd, 1, executable)
|
|
end
|
|
return cmd
|
|
end
|
|
|
|
function M.compile(language_config, substitutions)
|
|
if not language_config.compile then
|
|
return { code = 0, stdout = '' }
|
|
end
|
|
|
|
local cmd = substitute_template(language_config.compile, substitutions)
|
|
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
|
|
|
local t0 = vim.uv.hrtime()
|
|
local r = vim.system({ 'sh', '-c', sh }, { text = false }):wait()
|
|
local dt = (vim.uv.hrtime() - t0) / 1e6
|
|
|
|
local ansi = require('cp.ui.ansi')
|
|
r.stdout = ansi.bytes_to_string(r.stdout or '')
|
|
|
|
if r.code == 0 then
|
|
logger.log(('Compilation successful in %.1fms.'):format(dt), vim.log.levels.INFO)
|
|
else
|
|
logger.log(('Compilation failed in %.1fms.'):format(dt))
|
|
end
|
|
|
|
return r
|
|
end
|
|
|
|
local function parse_and_strip_time_v(output)
|
|
local s = output or ''
|
|
local last_i, from = nil, 1
|
|
while true do
|
|
local i = string.find(s, 'Command being timed:', from, true)
|
|
if not i then
|
|
break
|
|
end
|
|
last_i, from = i, i + 1
|
|
end
|
|
if not last_i then
|
|
return s, 0
|
|
end
|
|
|
|
local k = last_i - 1
|
|
while k >= 1 do
|
|
local ch = s:sub(k, k)
|
|
if ch ~= ' ' and ch ~= '\t' then
|
|
break
|
|
end
|
|
k = k - 1
|
|
end
|
|
|
|
local head = s:sub(1, k)
|
|
local tail = s:sub(last_i)
|
|
|
|
local peak_kb = 0.0
|
|
for line in tail:gmatch('[^\n]+') do
|
|
local kb = line:match('Maximum resident set size %(kbytes%):%s*(%d+)')
|
|
if kb then
|
|
peak_kb = tonumber(kb) or 0
|
|
end
|
|
end
|
|
|
|
local peak_mb = peak_kb / 1024.0
|
|
return head, peak_mb
|
|
end
|
|
|
|
function M.run(cmd, stdin, timeout_ms, memory_mb)
|
|
local time_bin = utils.time_path()
|
|
local timeout_bin = utils.timeout_path()
|
|
|
|
local prog = table.concat(cmd, ' ')
|
|
local pre = {
|
|
('ulimit -v %d'):format(memory_mb * 1024),
|
|
}
|
|
local prefix = table.concat(pre, '; ') .. '; '
|
|
local sec = math.ceil(timeout_ms / 1000)
|
|
local timeout_prefix = ('%s -k 1s %ds '):format(timeout_bin, sec)
|
|
local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
|
|
|
|
local t0 = vim.uv.hrtime()
|
|
local r = vim
|
|
.system({ 'sh', '-c', sh }, {
|
|
stdin = stdin,
|
|
text = true,
|
|
})
|
|
:wait()
|
|
local dt = (vim.uv.hrtime() - t0) / 1e6
|
|
|
|
local code = r.code or 0
|
|
local raw = r.stdout or ''
|
|
local cleaned, peak_mb = parse_and_strip_time_v(raw)
|
|
local tled = code == 124
|
|
|
|
local signal = nil
|
|
if code >= 128 then
|
|
signal = constants.signal_codes[code]
|
|
end
|
|
|
|
local lower = (cleaned or ''):lower()
|
|
local oom_hint = lower:find('std::bad_alloc', 1, true)
|
|
or lower:find('cannot allocate memory', 1, true)
|
|
or lower:find('out of memory', 1, true)
|
|
or lower:find('oom', 1, true)
|
|
or lower:find('enomem', 1, true)
|
|
local near_cap = peak_mb >= (0.90 * memory_mb)
|
|
|
|
local mled = (peak_mb >= memory_mb) or near_cap or (oom_hint and not tled)
|
|
|
|
if tled then
|
|
logger.log(('Execution timed out in %.1fms.'):format(dt))
|
|
elseif mled then
|
|
logger.log(('Execution memory limit exceeded in %.1fms.'):format(dt))
|
|
elseif code ~= 0 then
|
|
logger.log(('Execution failed in %.1fms (exit code %d).'):format(dt, code))
|
|
else
|
|
logger.log(('Execution successful in %.1fms.'):format(dt))
|
|
end
|
|
|
|
return {
|
|
stdout = cleaned,
|
|
code = code,
|
|
time_ms = dt,
|
|
tled = tled,
|
|
mled = mled,
|
|
peak_mb = peak_mb,
|
|
signal = signal,
|
|
}
|
|
end
|
|
|
|
function M.compile_problem(contest_config, is_debug)
|
|
local state = require('cp.state')
|
|
local source_file = state.get_source_file()
|
|
if not source_file then
|
|
return { success = false, output = 'No source file found.' }
|
|
end
|
|
|
|
local language = get_language_from_file(source_file, contest_config)
|
|
local language_config = contest_config[language]
|
|
if not language_config then
|
|
return { success = false, output = ('No configuration for language %s.'):format(language) }
|
|
end
|
|
|
|
local binary_file = state.get_binary_file()
|
|
local substitutions = { source = source_file, binary = binary_file }
|
|
|
|
local chosen = (is_debug and language_config.debug) and language_config.debug
|
|
or language_config.compile
|
|
if not chosen then
|
|
return { success = true, output = nil }
|
|
end
|
|
|
|
local saved = language_config.compile
|
|
language_config.compile = chosen
|
|
local r = M.compile(language_config, substitutions)
|
|
language_config.compile = saved
|
|
|
|
if r.code ~= 0 then
|
|
return { success = false, output = r.stdout or 'unknown error' }
|
|
end
|
|
return { success = true, output = nil }
|
|
end
|
|
|
|
return M
|