fix(coding-agent/eval): surfaced the exception type and message in the
- Surface the exception type and message in the error display by prepending the formatted error string to the traceback array. - Prevent the host from hiding the actual error by ensuring the traceback is not empty, consistent with other language runners.
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the `eval` Julia kernel showing only runner-internal backtrace frames (`at top-level scope (./none:N)`, `at main (…runner-…jl:635)`) with no exception type or message, making cell errors undebuggable. The host renderer (`packages/coding-agent/src/eval/kernel-base.ts`) displays a non-empty `traceback` verbatim and only falls back to `ename: evalue` when it is empty; the Python and Ruby runners embed the rendered error in `traceback`, but the Julia runner (`packages/coding-agent/src/eval/jl/runner.jl`) built `traceback` from stack frames only, so the message was dropped. `emit_error` now seeds `traceback` with the `showerror` output (matching the REPL's `ERROR:` text) ahead of the frames.
|
||||
|
||||
## [16.1.22] - 2026-06-26
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -45,4 +45,22 @@ nothing
|
||||
expect(result.output).toContain("META=alpha:true");
|
||||
expect(result.output).toContain("MULTI=2:alpha:json");
|
||||
}, 30_000);
|
||||
|
||||
it("surfaces the exception type and message in the error output, not just stack frames", async () => {
|
||||
using tempDir = TempDir.createSync("@omp-eval-julia-error-");
|
||||
const result = await executeJulia(`println("="^8)\nmissing_var_xyz + 1`, {
|
||||
cwd: tempDir.path(),
|
||||
sessionId: `julia-prelude-error:${crypto.randomUUID()}`,
|
||||
kernelOwnerId: OWNER_ID,
|
||||
reset: true,
|
||||
});
|
||||
|
||||
// The rendered error must carry the actual exception, not only the
|
||||
// runner-internal backtrace frames (regression: traceback-only output
|
||||
// hid `ename`/`evalue`).
|
||||
expect(result.output).toContain("UndefVarError");
|
||||
expect(result.output).toContain("missing_var_xyz");
|
||||
// Frames are still present alongside the message.
|
||||
expect(result.output).toContain("top-level scope");
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
@@ -522,7 +522,13 @@ function emit_error(rid, err, bt)
|
||||
end
|
||||
err_str = String(take!(io))
|
||||
|
||||
tb = String[]
|
||||
# Seed the traceback with the rendered exception text so the array is a
|
||||
# self-contained error display, matching the Python and Ruby runners. The
|
||||
# host shows `traceback` verbatim when present and only falls back to
|
||||
# `ename: evalue` when it is empty, so a frames-only traceback would hide
|
||||
# the real error. Julia's `showerror` output already embeds the exception
|
||||
# type for nearly every error and mirrors what the REPL prints after `ERROR: `.
|
||||
tb = isempty(err_str) ? String[] : String[err_str]
|
||||
for frame in stacktrace(bt)
|
||||
file = string(frame.file)
|
||||
line = frame.line
|
||||
|
||||
Executable
+357
@@ -0,0 +1,357 @@
|
||||
#!/usr/bin/env julia
|
||||
# bash_corpus_stats.jl
|
||||
# Extract every bash execution from ~/.omp/stats.db and analyze it.
|
||||
# sources: (1) bash-tool calls (2) full-file writes to *.sh
|
||||
# Reports: (1) bash constructs (2) CLI utilities (3) flags per utility
|
||||
#
|
||||
# julia bash_corpus_stats.jl
|
||||
# deps: import Pkg; Pkg.add(["SQLite","JSON3","DBInterface"])
|
||||
|
||||
using SQLite, JSON3, DBInterface, Printf
|
||||
|
||||
# ─────────────────────────── extraction ───────────────────────────
|
||||
const DB = SQLite.DB(joinpath(homedir(), ".omp", "stats.db"))
|
||||
|
||||
field(j, k) = try
|
||||
v = get(JSON3.read(j), k, nothing); v isa AbstractString ? String(v) : nothing
|
||||
catch; nothing end
|
||||
|
||||
function extract()
|
||||
bash, sh = String[], String[]
|
||||
for r in DBInterface.execute(DB, "SELECT arg_json FROM ss_tool_calls WHERE tool_name IN " *
|
||||
"('bash','Bash','shell','bash_exec','execute_command','run_command')")
|
||||
c = field(r.arg_json, :command); c !== nothing && !isempty(strip(c)) && push!(bash, c)
|
||||
end
|
||||
for r in DBInterface.execute(DB, "SELECT arg_json FROM ss_tool_calls WHERE tool_name IN " *
|
||||
"('write','write_file','create_file','file_write','replace_file','edit_file') " *
|
||||
"AND json_extract(arg_json,'\$.path') LIKE '%.sh'")
|
||||
c = field(r.arg_json, :content); c !== nothing && !isempty(strip(c)) && push!(sh, c)
|
||||
end
|
||||
return bash, sh
|
||||
end
|
||||
|
||||
# ─────────────────────────── lexer ───────────────────────────
|
||||
# Splits a bash string into words + control/redirection operators, tracks
|
||||
# expansion/construct features, and captures the inner text of command &
|
||||
# process substitutions (and shell-source heredoc bodies) for recursion.
|
||||
const WS = (' ', '\t')
|
||||
const SHELLS = Set(["sh","bash","zsh","dash","ksh","mksh","ash"])
|
||||
struct Tok; kind::Symbol; text::String; end # :W word :O operator :C comment
|
||||
|
||||
function read_balanced(cs::Vector{Char}, j::Int, openc::Char, closec::Char)
|
||||
n = length(cs); depth = 1; buf = Char[]
|
||||
while j <= n && depth > 0
|
||||
c = cs[j]
|
||||
if c == '\\' && j+1 <= n
|
||||
push!(buf, c, cs[j+1]); j += 2; continue
|
||||
elseif c == '\''
|
||||
k = j+1; while k <= n && cs[k] != '\''; k += 1; end
|
||||
if k > n; append!(buf, @view cs[j:n]); return String(buf), n+1; end
|
||||
append!(buf, @view cs[j:k]); j = k+1; continue
|
||||
elseif c == '"'
|
||||
k = j+1
|
||||
while k <= n && cs[k] != '"'; cs[k] == '\\' ? (k += 2) : (k += 1); end
|
||||
kk = min(k, n); append!(buf, @view cs[j:kk]); j = kk+1; continue
|
||||
end
|
||||
if c == openc; depth += 1
|
||||
elseif c == closec; depth -= 1; depth == 0 && return String(buf), j+1
|
||||
end
|
||||
push!(buf, c); j += 1
|
||||
end
|
||||
return String(buf), j
|
||||
end
|
||||
|
||||
function lex(s::String)
|
||||
cs = collect(s); n = length(cs)
|
||||
toks = Tok[]; feats = String[]; subs = String[]
|
||||
hd_queue = Tuple{String,Bool}[]
|
||||
word = Char[]; started = false
|
||||
expect_delim::Union{Nothing,Bool} = nothing
|
||||
i = 1
|
||||
|
||||
flush!() = begin
|
||||
if started
|
||||
w = String(word)
|
||||
if expect_delim !== nothing
|
||||
d = replace(strip(strip(w), ['\'','"']), "\\"=>"")
|
||||
push!(hd_queue, (String(d), expect_delim)); expect_delim = nothing
|
||||
end
|
||||
push!(toks, Tok(:W, w))
|
||||
end
|
||||
empty!(word); started = false
|
||||
end
|
||||
line_is_shell() = begin
|
||||
e = length(toks) - 1; st = e - 1
|
||||
while st >= 1 && !(toks[st].kind == :O && toks[st].text == "\n"); st -= 1; end
|
||||
for idx in (st+1):e
|
||||
t = toks[idx]
|
||||
t.kind == :W && (last(split(t.text, '/')) in SHELLS) && return true
|
||||
if t.kind == :O && (t.text == ">" || t.text == ">>") && idx < e
|
||||
nt = toks[idx+1]
|
||||
nt.kind == :W && endswith(strip(nt.text, ['\'','"']), ".sh") && return true
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
consume_heredocs(j) = begin
|
||||
shell = line_is_shell()
|
||||
for (delim, stripmode) in hd_queue
|
||||
j += 1; body = Char[]; first = true
|
||||
while j <= n
|
||||
e = j; while e <= n && cs[e] != '\n'; e += 1; end
|
||||
ln = String(@view cs[j:min(e-1,n)])
|
||||
test = stripmode ? lstrip(ln, '\t') : ln
|
||||
if test == delim; j = e; break; end
|
||||
first ? (first = false) : push!(body, '\n'); append!(body, collect(ln))
|
||||
if e > n; j = n+1; break; end
|
||||
j = e + 1
|
||||
end
|
||||
shell && !isempty(body) && push!(subs, String(body))
|
||||
end
|
||||
empty!(hd_queue); j
|
||||
end
|
||||
|
||||
while i <= n
|
||||
c = cs[i]
|
||||
if c == '\\' && i+1 <= n && cs[i+1] == '\n'; i += 2; continue; end
|
||||
if c == '\\' && i+1 <= n; push!(word, c, cs[i+1]); started = true; i += 2; continue; end
|
||||
if c in WS; flush!(); i += 1; continue; end
|
||||
if c == '\n'
|
||||
flush!(); push!(toks, Tok(:O, "\n"))
|
||||
i = isempty(hd_queue) ? i+1 : consume_heredocs(i); continue
|
||||
end
|
||||
if c == '#' && !started
|
||||
j = i; while j <= n && cs[j] != '\n'; j += 1; end
|
||||
push!(toks, Tok(:C, String(@view cs[i:j-1]))); i = j; continue
|
||||
end
|
||||
if c == '\''
|
||||
k = i+1; while k <= n && cs[k] != '\''; k += 1; end
|
||||
kk = min(k, n); append!(word, @view cs[i:kk]); started = true; i = kk+1; continue
|
||||
end
|
||||
if c == '"'
|
||||
j = i+1; push!(word, '"')
|
||||
while j <= n && cs[j] != '"'
|
||||
if cs[j] == '\\' && j+1 <= n; push!(word, cs[j], cs[j+1]); j += 2; continue; end
|
||||
if cs[j] == '$' && j+1 <= n && cs[j+1] == '('
|
||||
inner, j2 = read_balanced(cs, j+2, '(', ')')
|
||||
push!(feats, "cmdsub_\$()"); push!(subs, inner)
|
||||
append!(word, collect("\$(" * inner * ")")); j = j2; continue
|
||||
end
|
||||
if cs[j] == '$' && j+1 <= n && cs[j+1] == '{'
|
||||
inner, j2 = read_balanced(cs, j+2, '{', '}')
|
||||
push!(feats, "paramexp_\${}"); append!(word, collect("\${" * inner * "}")); j = j2; continue
|
||||
end
|
||||
if cs[j] == '`'
|
||||
k = j+1; while k <= n && cs[k] != '`'; cs[k] == '\\' ? (k += 2) : (k += 1); end
|
||||
kk = min(k, n); push!(feats, "cmdsub_backtick"); push!(subs, String(@view cs[j+1:min(kk-1,n)]))
|
||||
append!(word, @view cs[j:kk]); j = kk+1; continue
|
||||
end
|
||||
cs[j] == '$' && push!(feats, "var_in_dq")
|
||||
push!(word, cs[j]); j += 1
|
||||
end
|
||||
push!(word, '"'); started = true; i = min(j+1, n+1); continue
|
||||
end
|
||||
if c == '$' && i+1 <= n
|
||||
nx = cs[i+1]
|
||||
if nx == '('
|
||||
if i+2 <= n && cs[i+2] == '('
|
||||
inner, j = read_balanced(cs, i+3, '(', ')')
|
||||
push!(feats, "arith_\$(())"); append!(word, collect("\$((" * inner * "))")); started = true
|
||||
i = (j <= n && cs[j] == ')') ? j+1 : j; continue
|
||||
end
|
||||
inner, j = read_balanced(cs, i+2, '(', ')')
|
||||
push!(feats, "cmdsub_\$()"); push!(subs, inner)
|
||||
append!(word, collect("\$(" * inner * ")")); started = true; i = j; continue
|
||||
end
|
||||
if nx == '{'
|
||||
inner, j = read_balanced(cs, i+2, '{', '}')
|
||||
push!(feats, "paramexp_\${}"); append!(word, collect("\${" * inner * "}")); started = true; i = j; continue
|
||||
end
|
||||
if nx == '\''
|
||||
k = i+2; while k <= n && cs[k] != '\''; cs[k] == '\\' ? (k += 2) : (k += 1); end
|
||||
kk = min(k, n); push!(feats, "ansi_c_quote"); append!(word, @view cs[i:kk]); started = true; i = kk+1; continue
|
||||
end
|
||||
push!(feats, "var_\$"); push!(word, '$'); started = true; i += 1; continue
|
||||
end
|
||||
if c == '`'
|
||||
k = i+1; while k <= n && cs[k] != '`'; cs[k] == '\\' ? (k += 2) : (k += 1); end
|
||||
kk = min(k, n); push!(feats, "cmdsub_backtick"); push!(subs, String(@view cs[i+1:min(kk-1,n)]))
|
||||
append!(word, @view cs[i:kk]); started = true; i = kk+1; continue
|
||||
end
|
||||
if c in ('|','&',';','<','>','(',')')
|
||||
flush!()
|
||||
if (c == '<' || c == '>') && i+1 <= n && cs[i+1] == '('
|
||||
inner, j = read_balanced(cs, i+2, '(', ')')
|
||||
push!(feats, c == '<' ? "procsub_<()" : "procsub_>()"); push!(subs, inner)
|
||||
push!(toks, Tok(:W, string(c) * "(" * inner * ")")); i = j; continue
|
||||
end
|
||||
three = String(@view cs[i:min(i+2,n)]); two = String(@view cs[i:min(i+1,n)])
|
||||
if three == "<<<"; push!(toks, Tok(:O, "<<<")); i += 3; continue; end
|
||||
if two == "<<"
|
||||
op = three == "<<-" ? "<<-" : "<<"
|
||||
push!(toks, Tok(:O, op)); expect_delim = (op == "<<-"); i += length(op); continue
|
||||
end
|
||||
if three == "&>>"; push!(toks, Tok(:O, "&>>")); i += 3; continue; end
|
||||
if two in ("&&","||",";;","|&",">>","<&",">&","&>","<>"); push!(toks, Tok(:O, two)); i += 2; continue; end
|
||||
push!(toks, Tok(:O, string(c))); i += 1; continue
|
||||
end
|
||||
push!(word, c); started = true; i += 1
|
||||
end
|
||||
flush!()
|
||||
return toks, feats, subs
|
||||
end
|
||||
|
||||
# ─────────────────────────── analyzer ───────────────────────────
|
||||
const KW = Set(["if","then","else","elif","fi","for","while","until","do","done",
|
||||
"case","esac","select","function","in","time","coproc"])
|
||||
const WRAPPERS = Set(["sudo","env","timeout","command","builtin","exec","nohup","nice",
|
||||
"ionice","stdbuf","setsid","doas","xargs","watch"])
|
||||
const REDIRS = Set(["<",">",">>","<<","<<-","<<<","<&",">&","&>","&>>","<>"])
|
||||
const OPC = Dict(
|
||||
"|"=>"pipe |","|&"=>"pipe |&","&&"=>"and &&","||"=>"or ||",";"=>"seq ;",";;"=>"case-clause ;;",
|
||||
"&"=>"background &",">"=>"redir >",">>"=>"redir-append >>","<"=>"redir-in <","<&"=>"fd-dup <&",
|
||||
">&"=>"fd-dup >&","&>"=>"redir-both &>","&>>"=>"redir-both-append &>>","<>"=>"redir-rw <>",
|
||||
"<<<"=>"herestring <<<","<<"=>"heredoc <<","<<-"=>"heredoc <<-")
|
||||
const FEATC = Dict(
|
||||
"cmdsub_\$()"=>"cmdsub \$()","cmdsub_backtick"=>"cmdsub ``","arith_\$(())"=>"arith-exp \$(())",
|
||||
"paramexp_\${}"=>"paramexp \${}","procsub_<()"=>"procsub <()","procsub_>()"=>"procsub >()",
|
||||
"var_\$"=>"var \$x","var_in_dq"=>"var-in-dquotes","ansi_c_quote"=>"ansi-c \$'..'")
|
||||
|
||||
inc!(d, k) = (d[k] = get(d, k, 0) + 1)
|
||||
|
||||
function normutil(w::AbstractString)
|
||||
isempty(w) && return nothing
|
||||
(startswith(w,"\$")||startswith(w,"`")||occursin("\$(",w)||occursin("\${",w)||
|
||||
startswith(w,"\"")||startswith(w,"'")) && return "<dynamic>"
|
||||
base = String(last(split(w, '/')))
|
||||
isempty(base) ? nothing : base
|
||||
end
|
||||
function recordflag!(flags, util, w)
|
||||
util == "" && return
|
||||
f = w; eq = findfirst('=', f); eq !== nothing && (f = f[1:prevind(f,eq)])
|
||||
(isempty(f) || f == "-" || f == "--") && return
|
||||
d = get!(flags, util, Dict{String,Int}()); d[f] = get(d,f,0)+1
|
||||
end
|
||||
function dwc!(w, constructs)
|
||||
u = replace(w, r"'[^']*'"=>"", "\""=>"")
|
||||
startswith(w, "~") && inc!(constructs, "tilde ~")
|
||||
(occursin(r"\{[^{}]*\.\.[^{}]*\}", u) || occursin(r"\{[^{}]*,[^{}]*\}", u)) && inc!(constructs, "brace-expansion {}")
|
||||
(occursin('*', u) || occursin('?', u) || occursin(r"\[[^]]+\]", u)) && inc!(constructs, "glob */?/[]")
|
||||
end
|
||||
function unquote_script(w) # strip outer quotes of a `bash -c '<script>'` arg
|
||||
n = lastindex(w)
|
||||
if length(w) >= 2 && ((startswith(w,"'") && endswith(w,"'")) || (startswith(w,"\"") && endswith(w,"\"")))
|
||||
return w[nextind(w,1):prevind(w,n)]
|
||||
elseif startswith(w,"-")
|
||||
return nothing
|
||||
else
|
||||
return w
|
||||
end
|
||||
end
|
||||
|
||||
function analyze!(s::String, constructs, utils, flags, depth::Int=0)
|
||||
depth > 25 && return
|
||||
toks, feats, subs = lex(s)
|
||||
for f in feats; inc!(constructs, get(FEATC, f, f)); end
|
||||
expect = true; skip_target = 0; curutil = ""; wrapper = false; ddash = false; want_script = false
|
||||
i = 1; N = length(toks)
|
||||
while i <= N
|
||||
t = toks[i]
|
||||
if t.kind == :O
|
||||
op = t.text
|
||||
if op == "("
|
||||
if i < N && toks[i+1].kind == :O && toks[i+1].text == "("
|
||||
inc!(constructs,"arith-cmd (( ))"); i+=2; expect=false; curutil=""; ddash=false; want_script=false; continue
|
||||
end
|
||||
inc!(constructs,"subshell ( )"); expect=true; curutil=""; ddash=false; want_script=false; i+=1; continue
|
||||
elseif op == ")" || op == "\n"
|
||||
expect=true; curutil=""; ddash=false; want_script=false; i+=1; continue
|
||||
elseif haskey(OPC, op)
|
||||
inc!(constructs, OPC[op])
|
||||
if op in ("|","|&","&&","||",";",";;","&"); expect=true; curutil=""; ddash=false; want_script=false
|
||||
else; skip_target += 1; end
|
||||
i+=1; continue
|
||||
else; i+=1; continue; end
|
||||
elseif t.kind == :C
|
||||
i+=1; continue
|
||||
end
|
||||
w = t.text
|
||||
if skip_target > 0; skip_target -= 1; i+=1; continue; end
|
||||
if expect && occursin(r"^\d+$", w) && i < N && toks[i+1].kind==:O && toks[i+1].text in REDIRS
|
||||
i+=1; continue # leading fd (2>&1)
|
||||
end
|
||||
if expect
|
||||
if w in KW
|
||||
inc!(constructs,"kw "*w); expect = !(w in ("for","select","case")); i+=1; continue
|
||||
elseif w == "{"; inc!(constructs,"group { }"); expect=true; i+=1; continue
|
||||
elseif w == "}"; expect=true; i+=1; continue
|
||||
elseif w == "!"; inc!(constructs,"negation !"); expect=true; i+=1; continue
|
||||
elseif startswith(w,"[["); inc!(constructs,"test [[ ]]"); expect=false; i+=1; continue
|
||||
elseif w == "["; inc!(constructs,"test [ ]"); expect=false; i+=1; continue
|
||||
elseif occursin(r"^[A-Za-z_][A-Za-z0-9_]*(\[[^]]*\])?\+?=", w)
|
||||
inc!(constructs,"assignment VAR="); expect=true; i+=1; continue
|
||||
else
|
||||
u = normutil(w)
|
||||
if u !== nothing; inc!(utils,u); curutil=u; wrapper=(u in WRAPPERS)
|
||||
else; curutil=""; wrapper=false; end
|
||||
expect=false; ddash=false; want_script=false; i+=1; continue
|
||||
end
|
||||
else
|
||||
dwc!(w, constructs)
|
||||
if w == "--"; ddash = true; i+=1; continue; end # option terminator
|
||||
if want_script
|
||||
want_script = false
|
||||
src = unquote_script(w)
|
||||
src !== nothing && analyze!(String(src), constructs, utils, flags, depth+1)
|
||||
i+=1; continue
|
||||
end
|
||||
if wrapper # sudo/env/timeout/xargs: count, then find real subcommand
|
||||
if startswith(w,"-"); recordflag!(flags,curutil,w); i+=1; continue
|
||||
elseif occursin(r"^[A-Za-z_][A-Za-z0-9_]*=", w) || occursin(r"^\d+(\.\d+)?[smhd]?$", w); i+=1; continue
|
||||
else
|
||||
u2 = normutil(w)
|
||||
if u2 !== nothing; inc!(utils,u2); curutil=u2; wrapper=(u2 in WRAPPERS)
|
||||
else; curutil=""; wrapper=false; end
|
||||
ddash=false; i+=1; continue
|
||||
end
|
||||
end
|
||||
if !ddash && startswith(w,"-") && length(w)>1 && curutil!=""
|
||||
recordflag!(flags,curutil,w)
|
||||
(curutil in SHELLS && occursin(r"^-[a-z]*c$", w)) && (want_script = true) # bash -c <script>
|
||||
end
|
||||
i+=1; continue
|
||||
end
|
||||
end
|
||||
for sub in subs; analyze!(sub, constructs, utils, flags, depth+1); end
|
||||
end
|
||||
|
||||
# ─────────────────────────── reporting ───────────────────────────
|
||||
sortd(d) = sort(collect(d), by=x->-x[2])
|
||||
|
||||
function main()
|
||||
bash, sh = extract()
|
||||
corpus = vcat(bash, sh); total = length(corpus)
|
||||
constructs = Dict{String,Int}(); utils = Dict{String,Int}(); flags = Dict{String,Dict{String,Int}}()
|
||||
for s in corpus; analyze!(s, constructs, utils, flags); end
|
||||
|
||||
println("corpus: $(length(bash)) bash calls + $(length(sh)) .sh writes = $total snippets\n")
|
||||
println("="^60, "\n1) BASH CONSTRUCTS\n", "="^60)
|
||||
for (k,v) in sortd(constructs)
|
||||
@printf(" %-26s %9d %5.1f%%\n", k, v, 100v/total)
|
||||
end
|
||||
ut = sum(values(utils))
|
||||
println("\n", "="^60, "\n2) CLI UTILITIES ($(length(utils)) distinct)\n", "="^60)
|
||||
for (k,v) in sortd(utils)[1:min(60,length(utils))]
|
||||
@printf(" %-22s %9d %5.1f%%\n", k, v, 100v/ut)
|
||||
end
|
||||
println("\n", "="^60, "\n3) FLAGS PER UTILITY\n", "="^60)
|
||||
flagtot = Dict(u => sum(values(d)) for (u,d) in flags)
|
||||
for (u, ft) in sort(collect(flagtot), by=x->-x[2])[1:min(30,length(flagtot))]
|
||||
fl = sortd(flags[u]); shown = fl[1:min(12,length(fl))]
|
||||
println(" ", rpad(u,12), "[$ft uses, $(length(fl)) distinct]")
|
||||
println(" ", join(["$k·$v" for (k,v) in shown], " "),
|
||||
length(fl) > 12 ? " (+$(length(fl)-12))" : "")
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user