载入中...
搜索中...
未找到
ConsolePanel.cpp
浏览该文件的文档.
4
5#include "common/CrashLog.h"
8
9#include <simplesquirrel/simplesquirrel.hpp>
10#include <squirrel.h>
11
12#include <algorithm>
13#include <chrono>
14#include <cstdarg>
15#include <cstdio>
16#include <ctime>
17#include <sstream>
18#include <string>
19#include <utility>
20#include <vector>
21
22namespace eve::dev {
23namespace {
24
25ConsolePanel::ImGuiDrawer g_imguiDrawer = nullptr;
26
27// Original Squirrel print/error callbacks (installed by the VM / std lib).
28SQPRINTFUNCTION g_prevPrint = nullptr;
29SQPRINTFUNCTION g_prevError = nullptr;
30
31void forwardPrint(HSQUIRRELVM v, const SQChar* text) {
32 if (g_prevPrint) {
33 g_prevPrint(v, "%s", text ? text : "");
34 } else {
35 std::fputs(text ? text : "", stdout);
36 }
37}
38
39void forwardError(HSQUIRRELVM v, const SQChar* text) {
40 if (g_prevError) {
41 g_prevError(v, "%s", text ? text : "");
42 } else {
43 std::fputs(text ? text : "", stderr);
44 }
45}
46
47// One console line has to survive a full debug dump: the previous fixed 1 KiB
48// buffer silently cut long prints, and truncated output is exactly what an
49// unattended agent cannot afford to miss. Over-long lines are now truncated
50// explicitly, and the marker says how much was dropped.
51constexpr std::size_t kMaxCapturedLine = 64 * 1024;
52
53std::string formatCaptured(const SQChar* s, va_list args) {
54 const char* format = s ? s : "";
55 va_list probe;
56 va_copy(probe, args);
57 const int needed = std::vsnprintf(nullptr, 0, format, probe);
58 va_end(probe);
59 if (needed < 0) return std::string(format);
60
61 const std::size_t size = static_cast<std::size_t>(needed);
62 const std::size_t keep = std::min(size, kMaxCapturedLine);
63 std::vector<char> buffer(keep + 1, '\0');
64 std::vsnprintf(buffer.data(), buffer.size(), format, args);
65 std::string text(buffer.data(), keep);
66 if (size > keep) {
67 text += "\n[console: truncated " + std::to_string(size - keep) + " of " + std::to_string(size) + " bytes]";
68 }
69 return text;
70}
71
72// Squirrel print callback (varargs, printf-style). Captured into the console
73// log, then forwarded to the previous handler so stdout/stderr behavior stays.
74void capturePrint(HSQUIRRELVM v, const SQChar* s, ...) {
75 va_list args;
76 va_start(args, s);
77 const std::string text = formatCaptured(s, args);
78 va_end(args);
80 forwardPrint(v, text.c_str());
81}
82
83void captureError(HSQUIRRELVM v, const SQChar* s, ...) {
84 va_list args;
85 va_start(args, s);
86 const std::string text = formatCaptured(s, args);
87 va_end(args);
89 forwardError(v, text.c_str());
90}
91
92std::string typeName(HSQUIRRELVM vm, SQInteger idx) {
93 switch (sq_gettype(vm, idx)) {
94 case OT_NULL: return "null";
95 case OT_INTEGER: return "integer";
96 case OT_FLOAT: return "float";
97 case OT_BOOL: return "bool";
98 case OT_STRING: return "string";
99 case OT_TABLE: return "table";
100 case OT_ARRAY: return "array";
101 case OT_CLOSURE: return "closure";
102 case OT_NATIVECLOSURE: return "native";
103 case OT_USERDATA: return "userdata";
104 case OT_CLASS: return "class";
105 case OT_INSTANCE: return "instance";
106 case OT_THREAD: return "thread";
107 case OT_GENERATOR: return "generator";
108 case OT_WEAKREF: return "weakref";
109 default: return "other";
110 }
111}
112
113std::string formatValue(HSQUIRRELVM vm, SQInteger idx) {
114 switch (sq_gettype(vm, idx)) {
115 case OT_NULL: return "null";
116 case OT_BOOL: {
117 SQBool b = SQFalse;
118 sq_getbool(vm, idx, &b);
119 return b ? "true" : "false";
120 }
121 case OT_INTEGER: {
122 SQInteger v = 0;
123 sq_getinteger(vm, idx, &v);
124 return std::to_string(static_cast<long long>(v));
125 }
126 case OT_FLOAT: {
127 SQFloat v = 0;
128 sq_getfloat(vm, idx, &v);
129 std::ostringstream oss;
130 oss << static_cast<double>(v);
131 return oss.str();
132 }
133 case OT_STRING: {
134 const SQChar* s = nullptr;
135 sq_getstring(vm, idx, &s);
136 return std::string("\"") + (s ? s : "") + "\"";
137 }
138 case OT_TABLE: return "<table>";
139 case OT_ARRAY: return "<array>";
140 case OT_CLOSURE: return "<closure>";
141 case OT_NATIVECLOSURE: return "<native>";
142 case OT_USERDATA: return "<userdata>";
143 case OT_INSTANCE: return "<instance>";
144 case OT_CLASS: return "<class>";
145 case OT_THREAD: return "<thread>";
146 default: return "<" + typeName(vm, idx) + ">";
147 }
148}
149
150} // namespace
151
153 // Process-immortal singleton; see devtools/Immortal.hpp.
155}
156
157std::string ConsolePanel::nowStamp() {
158 using clock = std::chrono::system_clock;
159 const auto t = clock::to_time_t(clock::now());
160 std::tm tm{};
161#if defined(_WIN32)
162 localtime_s(&tm, &t);
163#else
164 localtime_r(&t, &tm);
165#endif
166 char buf[32];
167 std::strftime(buf, sizeof(buf), "%H:%M:%S", &tm);
168 return buf;
169}
170
172 std::lock_guard<std::mutex> lock(mu_);
173 visible_ = on;
174}
175
177 std::lock_guard<std::mutex> lock(mu_);
178 return visible_;
179}
180
182
183void ConsolePanel::addLog(std::string level, std::string text) {
184 std::lock_guard<std::mutex> lock(mu_);
185 appendLocked(std::move(level), std::move(text));
186}
187
188void ConsolePanel::appendLocked(std::string level, std::string text) {
190 line.seq = nextSeq_++;
191 line.timestamp = nowStamp();
192 line.level = level;
193 line.text = text;
194 log_.push_back(std::move(line));
195 while (log_.size() > maxEntries_) {
196 droppedThrough_ = log_.front().seq;
197 log_.pop_front();
198 }
199 // Errors also go to the persistent crash/error log: after a crash the MCP
200 // session is gone, and eve.log is the only evidence of what went wrong.
201 if (level == "error") eve::recordLogEvent("error", text);
202}
203
204void ConsolePanel::addEngineLine(const std::string& text) {
205 if (text.empty()) return;
206 std::lock_guard<std::mutex> lock(mu_);
207 // capturePrint/captureError forward to stderr, so the descriptor-level
208 // capture sees the same text a moment later. Drop only that echo: a genuine
209 // repeat emitted by the engine itself keeps its own entry.
210 if (!log_.empty()) {
211 const ConsoleLine& previous = log_.back();
212 if (previous.text == text && (previous.level == "print" || previous.level == "error")) return;
213 }
214 appendLocked("engine", text);
215}
216
218
219void ConsolePanel::addInfo(std::string text) { addLog("info", std::move(text)); }
220
221void ConsolePanel::addWarn(std::string text) { addLog("warn", std::move(text)); }
222
223void ConsolePanel::addError(std::string text) { addLog("error", std::move(text)); }
224
226 std::lock_guard<std::mutex> lock(mu_);
227 // Dropping lines must advance the drop cursor, never the sequence cursor:
228 // an existing reader cursor stays meaningful and cannot re-read or stall.
229 droppedThrough_ = nextSeq_ == 0 ? 0 : nextSeq_ - 1;
230 log_.clear();
231}
232
234 std::lock_guard<std::mutex> lock(mu_);
235 maxEntries_ = n == 0 ? 1 : n;
236 while (log_.size() > maxEntries_) {
237 droppedThrough_ = log_.front().seq;
238 log_.pop_front();
239 }
240}
241
242std::uint64_t ConsolePanel::nextSeq() const {
243 std::lock_guard<std::mutex> lock(mu_);
244 return nextSeq_;
245}
246
247std::uint64_t ConsolePanel::droppedThrough() const {
248 std::lock_guard<std::mutex> lock(mu_);
249 return droppedThrough_;
250}
251
252ConsoleSlice ConsolePanel::read(std::uint64_t afterSeq, size_t max, const std::string& level) const {
253 std::lock_guard<std::mutex> lock(mu_);
254
255 ConsoleSlice slice;
256 slice.nextSeq = nextSeq_;
257 slice.droppedThrough = droppedThrough_;
258 slice.firstSeq = log_.empty() ? nextSeq_ : log_.front().seq;
259
260 const size_t want = max == 0 ? 64 : max;
261 if (afterSeq == 0) {
262 // Tail read: newest matching lines, returned oldest first.
263 slice.truncated = droppedThrough_ > 0;
264 for (auto it = log_.rbegin(); it != log_.rend() && slice.lines.size() < want; ++it) {
265 if (!level.empty() && it->level != level) continue;
266 slice.lines.push_back(*it);
267 }
268 std::reverse(slice.lines.begin(), slice.lines.end());
269 } else {
270 slice.truncated = afterSeq < droppedThrough_;
271 for (const auto& line : log_) {
272 if (line.seq <= afterSeq) continue;
273 if (!level.empty() && line.level != level) continue;
274 if (slice.lines.size() >= want) break;
275 slice.lines.push_back(line);
276 }
277 }
278 // The resumable cursor is the last line actually covered by this read. It is
279 // deliberately not `nextSeq`: that is the seq the *next* appended line will
280 // receive, and paging from it would skip exactly one line.
281 slice.cursor = slice.lines.empty() ? afterSeq : slice.lines.back().seq;
282 return slice;
283}
284
285std::vector<ConsoleLine> ConsolePanel::recent(size_t max) const {
286 std::lock_guard<std::mutex> lock(mu_);
287 std::vector<ConsoleLine> out;
288 if (log_.empty() || max == 0) return out;
289 const size_t start = log_.size() > max ? log_.size() - max : 0;
290 out.assign(log_.begin() + static_cast<std::ptrdiff_t>(start), log_.end());
291 return out;
292}
293
294std::string ConsolePanel::format(size_t max) const {
295 auto lines = recent(max);
296 std::ostringstream oss;
297 for (const auto& l : lines) {
298 oss << '[' << l.timestamp << "] " << l.level << " | " << l.text << '\n';
299 }
300 return oss.str();
301}
302
304 if (!vm) return;
305 detach();
306 vm_ = vm;
307 // Snapshot existing handlers once (they may be the stdlib defaults).
308 if (!g_prevPrint) g_prevPrint = sq_getprintfunc(vm);
309 if (!g_prevError) g_prevError = sq_geterrorfunc(vm);
310 sq_setprintfunc(vm, capturePrint, captureError);
311 addLog("info", "console attached to VM");
312 // Engine-side diagnostics (std::cerr and fprintf(stderr)) are the other half
313 // of a scripted game's output; mirror them into the same ordered stream.
314 const StderrCaptureStatus capture =
315 startStderrCapture([](const std::string& line) { ConsolePanel::instance().addEngineLine(line); });
316 // A handler-less capture must be observable, not silent: an agent that sees
317 // no `engine` lines has to know the coverage is missing rather than assume
318 // the engine printed nothing.
319 if (capture != StderrCaptureStatus::Active) {
320 addLog("warn", "engine stderr capture unavailable; only script print/error reaches this console");
321 }
322}
323
326 if (!vm_) return;
327 sq_setprintfunc(vm_, g_prevPrint, g_prevError);
328 vm_ = nullptr;
329 addLog("info", "console detached from VM");
330}
331
332std::string ConsolePanel::eval(const std::string& expression) {
333 if (expression.empty()) return "error: empty expression";
334 HSQUIRRELVM vm = vm_;
335 if (!vm) return "error: no VM attached";
336 addLog("cmd", expression);
337
338 const SQInteger top = sq_gettop(vm);
339 // Compile `return (expr);` so the evaluation result lands on the stack.
340 const std::string source = "return (" + expression + ");";
342 vm, source.c_str(), static_cast<SQInteger>(source.size()), _SC("console_repl.nut"), SQTrue))) {
343 sq_settop(vm, top);
345 const std::string err = "error: " +
346 (ctx.empty() ? std::string("compile failed")
348 addLog("error", err);
349 return err;
350 }
351 sq_pushroottable(vm);
352 if (SQ_FAILED(sq_call(vm, 1, SQTrue, SQTrue))) {
353 sq_settop(vm, top);
355 const std::string err = "error: " +
356 (ctx.empty() ? std::string("runtime failed")
358 addLog("error", err);
359 return err;
360 }
361 std::string result = formatValue(vm, -1);
362 sq_settop(vm, top);
363 addLog("result", result);
364 return result;
365}
366
368
370 // ImGui UI lives in eve_imgui (ImGuiBackend) so EVDevTools does not
371 // instantiate imgui.h inlines — that blew the MSVC 65535 export limit.
372 if (g_imguiDrawer) g_imguiDrawer(*this);
373}
374
375} // namespace eve::dev
Duration start
SQInteger top
const std::string & s
struct SQVM * HSQUIRRELVM
Persistent, thread-safe record of engine crash and problem events.
HSQUIRRELVM vm
Definition ECS.cpp:20
glm::vec3 n
Definition Grass.cpp:63
float v
std::string text
MeleePoint3 b
Definition MeleeHit.cpp:41
int level
graphics::Canvas * previous
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
int idx
float t
Mirror process stderr into an in-process line sink without hiding it.
float size
Definition TreeMesh.cpp:156
std::vector< std::string_view > lines
const UnitySourceAsset & source
In-engine runtime console / log ring buffer for DevTools.
std::uint64_t nextSeq() const
Sequence number the next appended line will receive.
std::vector< ConsoleLine > recent(size_t max=128) const
static void setImGuiDrawer(ImGuiDrawer fn)
void addWarn(std::string text)
std::uint64_t droppedThrough() const
Highest sequence number no longer retained (evicted or cleared).
void addLog(std::string level, std::string text)
Append a leveled log line (thread-safe).
void(*)(ConsolePanel &panel) ImGuiDrawer
void addEngineLine(const std::string &text)
Append one line captured from process stderr under level engine.
static ConsolePanel & instance()
std::string eval(const std::string &expression)
Evaluate a Squirrel expression against the root table and return a formatted result (or error message...
void addError(std::string text)
void setMaxEntries(size_t n)
void addInfo(std::string text)
bool isCapturingStderr() const
Whether process stderr is currently mirrored into this console.
void attach(HSQUIRRELVM vm)
Attach to a Squirrel VM: capture print/script errors into the log.
std::string format(size_t max=128) const
ConsoleSlice read(std::uint64_t afterSeq, size_t max=64, const std::string &level={}) const
Read retained lines relative to a sequence cursor.
void drawImGui()
Optional ImGui draw hook. Default no-op: console UI is registered by the host (see setImGuiDrawer) so...
static SQRESULT compileBuffer(HSQUIRRELVM vm, const SQChar *source, SQInteger size, const SQChar *sourceName, SQBool raiseError)
Unified raw-VM entry used by REPL, debugger, MCP, and editor adapters.
StderrCaptureStatus
Outcome of a capture request.
void stopStderrCapture()
Restore stderr and join the drain thread. No-op when inactive.
bool stderrCaptureActive()
Whether stderr is currently mirrored into a sink.
StderrCaptureStatus startStderrCapture(StderrLineSink sink)
Start mirroring stderr into sink.
std::string formatScriptError(const ScriptErrorContext &ctx)
Formats a context into a human-readable multi-line report.
ScriptErrorContext takeLastScriptError(HSQUIRRELVM vm)
Consumes and clears the last recorded error for a VM.
ScriptErrorContext captureCompileError(HSQUIRRELVM vm)
Captures the last compilation error recorded by the VM.
void recordLogEvent(const std::string &level, const std::string &message)
Appends a timestamped event line to the crash/error log.
Definition CrashLog.cpp:70
SettlementPipeline::Stage fn
One bounded read of the console ring buffer together with its cursors.
std::uint64_t droppedThrough
std::vector< ConsoleLine > lines
static T & get()
Returns the process-lifetime instance.
Definition Immortal.hpp:21
Structured snapshot of a script error: message, throw site and stack.
Definition ScriptError.h:25
bool empty() const noexcept
True when no error payload was captured.
Definition ScriptError.h:36