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, "&", "&amp;");
137 	boost::algorithm::replace_all(cmessage, "<", "&lt;");
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