feat(atcoder): extract submit helpers; add live status notifications (#294)
## Problem `_submit_sync` was a 170-line nested closure with `_solve_turnstile` and the browser-install block further nested inside it. Status events went to stderr, which `run_scraper()` silently discards, leaving the user with a 10–30s silent hang after credential entry. The NDJSON spawn path also lacked stdin support, so submit had no streaming path at all. ## Solution Extract `_TURNSTILE_JS`, `_solve_turnstile`, `_ensure_browser`, and `_submit_headless` to module level in `atcoder.py`; status events (`installing_browser`, `checking_login`, `logging_in`, `submitting`) now print to stdout as NDJSON. Add stdin pipe support to the NDJSON spawn path in `scraper.lua` and switch `M.submit` to streaming with an `on_status` callback. Wire `on_status` in `submit.lua` to fire `vim.notify` for each phase transition.
This commit is contained in:
parent
1bc0aa41b6
commit
c194f12eee
11 changed files with 863 additions and 116 deletions
|
|
@ -44,8 +44,17 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
return { success = false, error = msg }
|
||||
end
|
||||
|
||||
if subcommand == 'submit' then
|
||||
utils.setup_nix_submit_env()
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local cmd = utils.get_python_cmd(platform, plugin_path)
|
||||
local cmd
|
||||
if subcommand == 'submit' then
|
||||
cmd = utils.get_python_submit_cmd(platform, plugin_path)
|
||||
else
|
||||
cmd = utils.get_python_cmd(platform, plugin_path)
|
||||
end
|
||||
vim.list_extend(cmd, { subcommand })
|
||||
vim.list_extend(cmd, args)
|
||||
|
||||
|
|
@ -62,8 +71,16 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
end
|
||||
end
|
||||
|
||||
if subcommand == 'submit' and utils.is_nix_build() then
|
||||
env.UV_PROJECT_ENVIRONMENT = vim.fn.stdpath('cache') .. '/cp-nvim/submit-env'
|
||||
end
|
||||
|
||||
if opts and opts.ndjson then
|
||||
local uv = vim.uv
|
||||
local stdin_pipe = nil
|
||||
if opts.stdin then
|
||||
stdin_pipe = uv.new_pipe(false)
|
||||
end
|
||||
local stdout = uv.new_pipe(false)
|
||||
local stderr = uv.new_pipe(false)
|
||||
local buf = ''
|
||||
|
|
@ -71,7 +88,7 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
local handle
|
||||
handle = uv.spawn(cmd[1], {
|
||||
args = vim.list_slice(cmd, 2),
|
||||
stdio = { nil, stdout, stderr },
|
||||
stdio = { stdin_pipe, stdout, stderr },
|
||||
env = spawn_env_list(env),
|
||||
cwd = plugin_path,
|
||||
}, function(code, signal)
|
||||
|
|
@ -85,6 +102,9 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
if opts.on_exit then
|
||||
opts.on_exit({ success = (code == 0), code = code, signal = signal })
|
||||
end
|
||||
if stdin_pipe and not stdin_pipe:is_closing() then
|
||||
stdin_pipe:close()
|
||||
end
|
||||
if not stdout:is_closing() then
|
||||
stdout:close()
|
||||
end
|
||||
|
|
@ -97,10 +117,21 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
end)
|
||||
|
||||
if not handle then
|
||||
if stdin_pipe and not stdin_pipe:is_closing() then
|
||||
stdin_pipe:close()
|
||||
end
|
||||
logger.log('Failed to start scraper process', vim.log.levels.ERROR)
|
||||
return { success = false, error = 'spawn failed' }
|
||||
end
|
||||
|
||||
if stdin_pipe then
|
||||
uv.write(stdin_pipe, opts.stdin, function()
|
||||
uv.shutdown(stdin_pipe, function()
|
||||
stdin_pipe:close()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
uv.read_start(stdout, function(_, data)
|
||||
if data == nil then
|
||||
if buf ~= '' and opts.on_event then
|
||||
|
|
@ -131,7 +162,12 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
return
|
||||
end
|
||||
|
||||
local sysopts = { text = true, timeout = 30000, env = env, cwd = plugin_path }
|
||||
local sysopts = {
|
||||
text = true,
|
||||
timeout = (subcommand == 'submit') and 120000 or 30000,
|
||||
env = env,
|
||||
cwd = plugin_path,
|
||||
}
|
||||
if opts and opts.stdin then
|
||||
sysopts.stdin = opts.stdin
|
||||
end
|
||||
|
|
@ -246,18 +282,39 @@ function M.scrape_all_tests(platform, contest_id, callback)
|
|||
})
|
||||
end
|
||||
|
||||
function M.submit(platform, contest_id, problem_id, language, source_code, credentials, callback)
|
||||
local creds_json = vim.json.encode(credentials)
|
||||
function M.submit(
|
||||
platform,
|
||||
contest_id,
|
||||
problem_id,
|
||||
language,
|
||||
source_code,
|
||||
credentials,
|
||||
on_status,
|
||||
callback
|
||||
)
|
||||
local done = false
|
||||
run_scraper(platform, 'submit', { contest_id, problem_id, language }, {
|
||||
ndjson = true,
|
||||
stdin = source_code,
|
||||
env_extra = { CP_CREDENTIALS = creds_json },
|
||||
on_exit = function(result)
|
||||
if type(callback) == 'function' then
|
||||
if result and result.success then
|
||||
callback(result.data or { success = true })
|
||||
else
|
||||
callback({ success = false, error = result and result.error or 'unknown' })
|
||||
env_extra = { CP_CREDENTIALS = vim.json.encode(credentials) },
|
||||
on_event = function(ev)
|
||||
if ev.status ~= nil then
|
||||
if type(on_status) == 'function' then
|
||||
on_status(ev.status)
|
||||
end
|
||||
elseif ev.success ~= nil then
|
||||
done = true
|
||||
if type(callback) == 'function' then
|
||||
callback(ev)
|
||||
end
|
||||
end
|
||||
end,
|
||||
on_exit = function(proc)
|
||||
if not done and type(callback) == 'function' then
|
||||
callback({
|
||||
success = false,
|
||||
error = 'submit process exited (code=' .. tostring(proc.code) .. ')',
|
||||
})
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ local cache = require('cp.cache')
|
|||
local logger = require('cp.log')
|
||||
local state = require('cp.state')
|
||||
|
||||
local STATUS_MSGS = {
|
||||
installing_browser = 'Installing browser (first time setup)...',
|
||||
checking_login = 'Checking login...',
|
||||
logging_in = 'Logging in...',
|
||||
submitting = 'Submitting...',
|
||||
}
|
||||
|
||||
local function prompt_credentials(platform, callback)
|
||||
local saved = cache.get_credentials(platform)
|
||||
if saved and saved.username and saved.password then
|
||||
|
|
@ -48,6 +55,8 @@ function M.submit(opts)
|
|||
local source_lines = vim.fn.readfile(source_file)
|
||||
local source_code = table.concat(source_lines, '\n')
|
||||
|
||||
vim.notify('[cp.nvim] Submitting...', vim.log.levels.INFO)
|
||||
|
||||
require('cp.scraper').submit(
|
||||
platform,
|
||||
contest_id,
|
||||
|
|
@ -55,6 +64,11 @@ function M.submit(opts)
|
|||
language,
|
||||
source_code,
|
||||
creds,
|
||||
function(status)
|
||||
vim.schedule(function()
|
||||
vim.notify('[cp.nvim] ' .. (STATUS_MSGS[status] or status), vim.log.levels.INFO)
|
||||
end)
|
||||
end,
|
||||
function(result)
|
||||
vim.schedule(function()
|
||||
if result and result.success then
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ local M = {}
|
|||
local logger = require('cp.log')
|
||||
|
||||
local _nix_python = nil
|
||||
local _nix_submit_cmd = nil
|
||||
local _nix_discovered = false
|
||||
|
||||
local uname = vim.uv.os_uname()
|
||||
|
|
@ -111,7 +112,83 @@ function M.get_python_cmd(module, plugin_path)
|
|||
return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
||||
end
|
||||
|
||||
---@param module string
|
||||
---@param plugin_path string
|
||||
---@return string[]
|
||||
function M.get_python_submit_cmd(module, plugin_path)
|
||||
if _nix_submit_cmd then
|
||||
return { _nix_submit_cmd, 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
||||
end
|
||||
return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
||||
end
|
||||
|
||||
local python_env_setup = false
|
||||
local _nix_submit_attempted = false
|
||||
|
||||
---@return boolean
|
||||
local function discover_nix_submit_cmd()
|
||||
local cache_dir = vim.fn.stdpath('cache') .. '/cp-nvim'
|
||||
local cache_file = cache_dir .. '/nix-submit'
|
||||
|
||||
local f = io.open(cache_file, 'r')
|
||||
if f then
|
||||
local cached = f:read('*l')
|
||||
f:close()
|
||||
if cached and vim.fn.executable(cached) == 1 then
|
||||
_nix_submit_cmd = cached
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local plugin_path = M.get_plugin_path()
|
||||
vim.cmd.redraw()
|
||||
vim.notify('Building submit environment...', vim.log.levels.INFO)
|
||||
vim.cmd.redraw()
|
||||
local result = vim
|
||||
.system(
|
||||
{ 'nix', 'build', plugin_path .. '#submitEnv', '--no-link', '--print-out-paths' },
|
||||
{ text = true }
|
||||
)
|
||||
:wait()
|
||||
|
||||
if result.code ~= 0 then
|
||||
logger.log('nix build #submitEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
local store_path = result.stdout:gsub('%s+$', '')
|
||||
local submit_cmd = store_path .. '/bin/cp-nvim-submit'
|
||||
|
||||
if vim.fn.executable(submit_cmd) ~= 1 then
|
||||
logger.log('nix submit cmd not executable at ' .. submit_cmd, vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
vim.fn.mkdir(cache_dir, 'p')
|
||||
f = io.open(cache_file, 'w')
|
||||
if f then
|
||||
f:write(submit_cmd)
|
||||
f:close()
|
||||
end
|
||||
|
||||
_nix_submit_cmd = submit_cmd
|
||||
return true
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.setup_nix_submit_env()
|
||||
if _nix_submit_cmd then
|
||||
return true
|
||||
end
|
||||
if _nix_submit_attempted then
|
||||
return false
|
||||
end
|
||||
_nix_submit_attempted = true
|
||||
if vim.fn.executable('nix') == 1 then
|
||||
return discover_nix_submit_cmd()
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
local function discover_nix_python()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue