1 /* Copyright (C) 2017 Wildfire Games.
2 * This file is part of 0 A.D.
3 *
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "precompiled.h"
19
20 #include "CLogger.h"
21
22 #include "graphics/FontMetrics.h"
23 #include "graphics/ShaderManager.h"
24 #include "graphics/TextRenderer.h"
25 #include "lib/ogl.h"
26 #include "lib/timer.h"
27 #include "lib/utf8.h"
28 #include "ps/CConsole.h"
29 #include "ps/Profile.h"
30 #include "renderer/Renderer.h"
31
32 #include <ctime>
33 #include <iostream>
34
35 #include <boost/algorithm/string/replace.hpp>
36
37 CStrW g_UniqueLogPostfix;
38 static const double RENDER_TIMEOUT = 10.0; // seconds before messages are deleted
39 static const double RENDER_TIMEOUT_RATE = 10.0; // number of timed-out messages deleted per second
40 static const size_t RENDER_LIMIT = 20; // maximum messages on screen at once
41
42 // Set up a default logger that throws everything away, because that's
43 // better than crashing. (This is particularly useful for unit tests which
44 // don't care about any log output.)
45 struct BlackHoleStreamBuf : public std::streambuf
46 {
47 } blackHoleStreamBuf;
48 std::ostream blackHoleStream(&blackHoleStreamBuf);
49 CLogger nullLogger(&blackHoleStream, &blackHoleStream, false, true);
50
51 CLogger* g_Logger = &nullLogger;
52
53 const char* html_header0 =
54 "<!DOCTYPE html>\n"
55 "<meta charset=\"utf-8\">\n"
56 "<title>Pyrogenesis Log</title>\n"
57 "<style>"
58 "body { background: #eee; color: black; font-family: sans-serif; } "
59 "p { background: white; margin: 3px 0 3px 0; } "
60 ".error { color: red; } "
61 ".warning { color: blue; }"
62 "</style>\n"
63 "<h2>0 A.D. (";
64
65 const char* html_header1 = "</h2>\n";
66
CLogger()67 CLogger::CLogger()
68 {
69 OsPath mainlogPath(psLogDir() / (L"mainlog" + g_UniqueLogPostfix + L".html"));
70 m_MainLog = new std::ofstream(OsString(mainlogPath).c_str(), std::ofstream::out | std::ofstream::trunc);
71 debug_printf("Writing the mainlog at %s\n", mainlogPath.string8().c_str());
72
73 OsPath interestinglogPath(psLogDir() / (L"interestinglog" + g_UniqueLogPostfix + L".html"));
74 m_InterestingLog = new std::ofstream(OsString(interestinglogPath).c_str(), std::ofstream::out | std::ofstream::trunc);
75
76 m_OwnsStreams = true;
77 m_UseDebugPrintf = true;
78
79 Init();
80 }
81
CLogger(std::ostream * mainLog,std::ostream * interestingLog,bool takeOwnership,bool useDebugPrintf)82 CLogger::CLogger(std::ostream* mainLog, std::ostream* interestingLog, bool takeOwnership, bool useDebugPrintf)
83 {
84 m_MainLog = mainLog;
85 m_InterestingLog = interestingLog;
86 m_OwnsStreams = takeOwnership;
87 m_UseDebugPrintf = useDebugPrintf;
88
89 Init();
90 }
91
Init()92 void CLogger::Init()
93 {
94 m_RenderLastEraseTime = -1.0;
95 // this is called too early to allow us to call timer_Time(),
96 // so we'll fill in the initial value later
97
98 m_NumberOfMessages = 0;
99 m_NumberOfErrors = 0;
100 m_NumberOfWarnings = 0;
101
102 *m_MainLog << html_header0 << engine_version << ") Main log" << html_header1;
103 *m_InterestingLog << html_header0 << engine_version << ") Main log (warnings and errors only)" << html_header1;
104 }
105
~CLogger()106 CLogger::~CLogger()
107 {
108 char buffer[128];
109 sprintf_s(buffer, ARRAY_SIZE(buffer), " with %d message(s), %d error(s) and %d warning(s).", m_NumberOfMessages,m_NumberOfErrors,m_NumberOfWarnings);
110
111 time_t t = time(NULL);
112 struct tm* now = localtime(&t);
113 char currentDate[17];
114 sprintf_s(currentDate, ARRAY_SIZE(currentDate), "%04d-%02d-%02d", 1900+now->tm_year, 1+now->tm_mon, now->tm_mday);
115 char currentTime[10];
116 sprintf_s(currentTime, ARRAY_SIZE(currentTime), "%02d:%02d:%02d", now->tm_hour, now->tm_min, now->tm_sec);
117
118 //Write closing text
119
120 *m_MainLog << "<p>Engine exited successfully on " << currentDate;
121 *m_MainLog << " at " << currentTime << buffer << "</p>\n";
122
123 *m_InterestingLog << "<p>Engine exited successfully on " << currentDate;
124 *m_InterestingLog << " at " << currentTime << buffer << "</p>\n";
125
126 if (m_OwnsStreams)
127 {
128 SAFE_DELETE(m_InterestingLog);
129 SAFE_DELETE(m_MainLog);
130 }
131 }
132
ToHTML(const char * message)133 static std::string ToHTML(const char* message)
134 {
135 std::string cmessage = message;
136 boost::algorithm::replace_all(cmessage, "&", "&");
137 boost::algorithm::replace_all(cmessage, "<", "<");
138 return cmessage;
139 }
140
WriteMessage(const char * message,bool doRender=false)141 void CLogger::WriteMessage(const char* message, bool doRender = false)
142 {
143 std::string cmessage = ToHTML(message);
144
145 CScopeLock lock(m_Mutex);
146
147 ++m_NumberOfMessages;
148 // if (m_UseDebugPrintf)
149 // debug_printf("MESSAGE: %s\n", message);
150
151 *m_MainLog << "<p>" << cmessage << "</p>\n";
152 m_MainLog->flush();
153
154 if (doRender)
155 {
156 if (g_Console) g_Console->InsertMessage(std::string("INFO: ") + message);
157
158 PushRenderMessage(Normal, message);
159 }
160 }
161
WriteError(const char * message)162 void CLogger::WriteError(const char* message)
163 {
164 std::string cmessage = ToHTML(message);
165
166 CScopeLock lock(m_Mutex);
167
168 ++m_NumberOfErrors;
169 if (m_UseDebugPrintf)
170 debug_printf("ERROR: %.16000s\n", message);
171
172 if (g_Console) g_Console->InsertMessage(std::string("ERROR: ") + message);
173 *m_InterestingLog << "<p class=\"error\">ERROR: " << cmessage << "</p>\n";
174 m_InterestingLog->flush();
175
176 *m_MainLog << "<p class=\"error\">ERROR: " << cmessage << "</p>\n";
177 m_MainLog->flush();
178
179 PushRenderMessage(Error, message);
180 }
181
WriteWarning(const char * message)182 void CLogger::WriteWarning(const char* message)
183 {
184 std::string cmessage = ToHTML(message);
185
186 CScopeLock lock(m_Mutex);
187
188 ++m_NumberOfWarnings;
189 if (m_UseDebugPrintf)
190 debug_printf("WARNING: %s\n", message);
191
192 if (g_Console) g_Console->InsertMessage(std::string("WARNING: ") + message);
193 *m_InterestingLog << "<p class=\"warning\">WARNING: " << cmessage << "</p>\n";
194 m_InterestingLog->flush();
195
196 *m_MainLog << "<p class=\"warning\">WARNING: " << cmessage << "</p>\n";
197 m_MainLog->flush();
198
199 PushRenderMessage(Warning, message);
200 }
201
Render()202 void CLogger::Render()
203 {
204 PROFILE3_GPU("logger");
205
206 CleanupRenderQueue();
207
208 CStrIntern font_name("mono-stroke-10");
209 CFontMetrics font(font_name);
210 int lineSpacing = font.GetLineSpacing();
211
212 CShaderTechniquePtr textTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text);
213 textTech->BeginPass();
214
215 CTextRenderer textRenderer(textTech->GetShader());
216 textRenderer.Font(font_name);
217 textRenderer.Color(1.0f, 1.0f, 1.0f);
218
219 // Offset by an extra 35px vertically to avoid the top bar.
220 textRenderer.Translate(4.0f, 35.0f + lineSpacing, 0.0f);
221
222 // (Lock must come after loading the CFont, since that might log error messages
223 // and attempt to lock the mutex recursively which is forbidden)
224 CScopeLock lock(m_Mutex);
225
226 for (const RenderedMessage& msg : m_RenderMessages)
227 {
228 const char* type;
229 if (msg.method == Normal)
230 {
231 type = "info";
232 textRenderer.Color(0.0f, 0.8f, 0.0f);
233 }
234 else if (msg.method == Warning)
235 {
236 type = "warning";
237 textRenderer.Color(1.0f, 1.0f, 0.0f);
238 }
239 else
240 {
241 type = "error";
242 textRenderer.Color(1.0f, 0.0f, 0.0f);
243 }
244
245 CMatrix3D savedTransform = textRenderer.GetTransform();
246
247 textRenderer.PrintfAdvance(L"[%8.3f] %hs: ", msg.time, type);
248 // Display the actual message in white so it's more readable
249 textRenderer.Color(1.0f, 1.0f, 1.0f);
250 textRenderer.Put(0.0f, 0.0f, msg.message.c_str());
251
252 textRenderer.SetTransform(savedTransform);
253
254 textRenderer.Translate(0.0f, (float)lineSpacing, 0.0f);
255 }
256
257 textRenderer.Render();
258
259 textTech->EndPass();
260 }
261
PushRenderMessage(ELogMethod method,const char * message)262 void CLogger::PushRenderMessage(ELogMethod method, const char* message)
263 {
264 double now = timer_Time();
265
266 // Add each message line separately
267 const char* pos = message;
268 const char* eol;
269 while ((eol = strchr(pos, '\n')) != NULL)
270 {
271 if (eol != pos)
272 {
273 RenderedMessage r = { method, now, std::string(pos, eol) };
274 m_RenderMessages.push_back(r);
275 }
276 pos = eol + 1;
277 }
278 // Add the last line, if we didn't end on a \n
279 if (*pos != '\0')
280 {
281 RenderedMessage r = { method, now, std::string(pos) };
282 m_RenderMessages.push_back(r);
283 }
284 }
285
CleanupRenderQueue()286 void CLogger::CleanupRenderQueue()
287 {
288 CScopeLock lock(m_Mutex);
289
290 if (m_RenderMessages.empty())
291 return;
292
293 double now = timer_Time();
294
295 // Initialise the timer on the first call (since we can't do it in the ctor)
296 if (m_RenderLastEraseTime == -1.0)
297 m_RenderLastEraseTime = now;
298
299 // Delete old messages, approximately at the given rate limit (and at most one per frame)
300 if (now - m_RenderLastEraseTime > 1.0/RENDER_TIMEOUT_RATE)
301 {
302 if (m_RenderMessages[0].time + RENDER_TIMEOUT < now)
303 {
304 m_RenderMessages.pop_front();
305 m_RenderLastEraseTime = now;
306 }
307 }
308
309 // If there's still too many then delete the oldest
310 if (m_RenderMessages.size() > RENDER_LIMIT)
311 m_RenderMessages.erase(m_RenderMessages.begin(), m_RenderMessages.end() - RENDER_LIMIT);
312 }
313
TestLogger()314 TestLogger::TestLogger()
315 {
316 m_OldLogger = g_Logger;
317 g_Logger = new CLogger(&m_Stream, &blackHoleStream, false, false);
318 }
319
~TestLogger()320 TestLogger::~TestLogger()
321 {
322 delete g_Logger;
323 g_Logger = m_OldLogger;
324 }
325
GetOutput()326 std::string TestLogger::GetOutput()
327 {
328 return m_Stream.str();
329 }
330
TestStdoutLogger()331 TestStdoutLogger::TestStdoutLogger()
332 {
333 m_OldLogger = g_Logger;
334 g_Logger = new CLogger(&std::cout, &blackHoleStream, false, false);
335 }
336
~TestStdoutLogger()337 TestStdoutLogger::~TestStdoutLogger()
338 {
339 delete g_Logger;
340 g_Logger = m_OldLogger;
341 }
342