Nux

Merge lp:~thumper/nux/rolling-file-appender into lp:nux

Proposed by Tim Penhey
Status: Merged
Approved by: Mirco Müller
Approved revision: 411
Merged at revision: 393
Proposed branch: lp:~thumper/nux/rolling-file-appender
Merge into: lp:nux
Diff against target: 1118 lines (+987/-6)
14 files modified
.bzrignore (+4/-2)
Nux/Makefile.am (+1/-1)
NuxCore/AsyncFileWriter.cpp (+246/-0)
NuxCore/AsyncFileWriter.h (+63/-0)
NuxCore/Makefile.am (+4/-0)
NuxCore/RollingFileAppender.cpp (+231/-0)
NuxCore/RollingFileAppender.h (+48/-0)
configure.ac (+1/-1)
tests/Helpers.cpp (+52/-0)
tests/Helpers.h (+63/-0)
tests/Makefile.am (+7/-2)
tests/test_async_file_writer.cpp (+110/-0)
tests/test_main.cpp (+10/-0)
tests/test_rolling_file_appender.cpp (+147/-0)
To merge this branch: bzr merge lp:~thumper/nux/rolling-file-appender
Reviewer Review Type Date Requested Status
Neil J. Patel (community) Approve
Mirco Müller (community) Approve
Review via email: mp+65314@code.launchpad.net

Description of the change

This branch adds a RollingFileAppender. It derives from std::ostream& and is
intended to be used with the logging code.

The RollingFileAppender is constructed with a full path filename, and
optionally a number of backup files and maximum log size.

Every time the stream is flushed, it checks the size of the file, and if
necessary, it closes the existing file, rolls the backups[1], and opens a new
file.

This change brings in the boost filesystem shared object library. It is 80k,
but I believe that it simplifies our filesystem handling and should be used.

The tests are found in tests/test_rolling_file_appender.cpp. The test uses a
fixture that has setup and teardown methods that make sure that the temp
directory doesn't exist prior to running the tests, and to clean up after
itself.

[1] Rolling means deleting the last log file if we have reached our limit on
the number of backup files, and renaming logfile.x to logfile.x+1, and finally
renaming logfile to logfile.1.

To post a comment you must log in.
398. By Tim Penhey

Found that I can simplify at least some of the tests.

Revision history for this message
Mirco Müller (macslow) wrote :

Trying to compile nux with this branch merged led to this error...

make[2]: Betrete Verzeichnis '/tmp/bla/nux/examples'
  CXXLD button
../NuxCore/.libs/libnux-core-1.0.so: undefined reference to `boost::filesystem::detail::remove_api(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
../NuxCore/.libs/libnux-core-1.0.so: undefined reference to `boost::system::get_system_category()'
../NuxCore/.libs/libnux-core-1.0.so: undefined reference to `boost::filesystem::detail::status_api(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, boost::system::error_code&)'
../NuxCore/.libs/libnux-core-1.0.so: undefined reference to `boost::filesystem::detail::file_size_api(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
../NuxCore/.libs/libnux-core-1.0.so: undefined reference to `boost::filesystem::detail::create_directory_api(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
../NuxCore/.libs/libnux-core-1.0.so: undefined reference to `boost::system::get_generic_category()'
../NuxCore/.libs/libnux-core-1.0.so: undefined reference to `boost::filesystem::detail::symlink_status_api(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, boost::system::error_code&)'
../NuxCore/.libs/libnux-core-1.0.so: undefined reference to `boost::filesystem::detail::rename_api(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
collect2: ld returned 1 exit status

review: Needs Fixing
Revision history for this message
Tim Penhey (thumper) wrote :

On Thu, 30 Jun 2011 13:11:12 you wrote:
> Review: Needs Fixing
> Trying to compile nux with this branch merged led to this error...

This branch is going to have hacked somewhat for gio stuff :) So is the remove
NFileName branch

399. By Tim Penhey

Initial declaration for the async file writer.

400. By Tim Penhey

Make the rolling file appender not depend on boost filesystem. Damn tests don't link though.

401. By Tim Penhey

Make the tests pass with the gio based rolling file appender.

402. By Tim Penhey

Framework of the async file writer and a failing test.

403. By Tim Penhey

Closer to working code now...

404. By Tim Penhey

Trying to figure out why it isn't creating the file.

405. By Tim Penhey

Tests should work, but they don't always.

406. By Tim Penhey

Async writer now seems to work.

407. By Tim Penhey

More ignores :(

408. By Tim Penhey

Merge trunk.

409. By Tim Penhey

Change the RollingFileAppender so it writes to the files asynchronously.

410. By Tim Penhey

Emit a signal when the files are rolled. Primarily for testing, but you never know.

411. By Tim Penhey

Update the rolling file appender tests.

Revision history for this message
Tim Penhey (thumper) wrote :

Hi Guys,

This is finally ready to have another look. I've kept the boost filesystem use, but only for the test suite. It allows for a secondary way to confirm that the files and directories and correct in the tests.

The RollingFileAppender now writes the log files out asynchronously using gio which is handled by the AsyncFileWriter class.

Due to the async nature, it was a PITA to test. I've added comments explaining the weirdness in the tests.

Revision history for this message
Mirco Müller (macslow) wrote :

After going through this again (this early in the morning) my brain hurts... but approved now :)

review: Approve
Revision history for this message
Neil J. Patel (njpatel) wrote :

Looks good and works well, nice one!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-06-30 13:26:34 +0000
3+++ .bzrignore 2011-07-15 04:02:32 +0000
4@@ -58,5 +58,7 @@
5 tests/gtest-nux-core
6 tests/test-nux
7 tests/test-nux-results.xml
8-
9-
10+tests/relative
11+Logs
12+**/nux.log
13+**/nux.log.*
14
15=== modified file 'Nux/Makefile.am'
16--- Nux/Makefile.am 2011-06-30 13:26:34 +0000
17+++ Nux/Makefile.am 2011-07-15 04:02:32 +0000
18@@ -23,7 +23,7 @@
19 $(NUX_LIBS)
20
21 libnux_@NUX_API_VERSION@_la_LDFLAGS = \
22- $(NUX_LT_LDFLAGS)
23+ $(NUX_LT_LDFLAGS) -lboost_filesystem
24
25 source_cpp = \
26 $(srcdir)/PropertyItem/CheckBoxProperty.cpp \
27
28=== added file 'NuxCore/AsyncFileWriter.cpp'
29--- NuxCore/AsyncFileWriter.cpp 1970-01-01 00:00:00 +0000
30+++ NuxCore/AsyncFileWriter.cpp 2011-07-15 04:02:32 +0000
31@@ -0,0 +1,246 @@
32+// -*- Mode: C++; indent-tabs-mode: nil; tab-width: 2 -*-
33+/*
34+ * Copyright 2011 Inalogic® Inc.
35+ *
36+ * This program is free software: you can redistribute it and/or modify it
37+ * under the terms of the GNU Lesser General Public License, as
38+ * published by the Free Software Foundation; either version 2.1 or 3.0
39+ * of the License.
40+ *
41+ * This program is distributed in the hope that it will be useful, but
42+ * WITHOUT ANY WARRANTY; without even the implied warranties of
43+ * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
44+ * PURPOSE. See the applicable version of the GNU Lesser General Public
45+ * License for more details.
46+ *
47+ * You should have received a copy of both the GNU Lesser General Public
48+ * License along with this program. If not, see <http://www.gnu.org/licenses/>
49+ *
50+ * Authored by: Tim Penhey <tim.penhey@canonical.com>
51+ *
52+ */
53+
54+#include "AsyncFileWriter.h"
55+
56+#include <sstream>
57+
58+#include <gio/gio.h>
59+
60+#include <iostream>
61+
62+namespace nux
63+{
64+
65+/**
66+ * CAUTION: right now this class is not thread aware. It assumes that all the
67+ * write calls are coming from a single thread. Perhaps we need to fix this?
68+ */
69+class AsyncFileWriter::Impl
70+{
71+public:
72+ Impl(AsyncFileWriter* owner, std::string const& filename);
73+ ~Impl();
74+
75+ void Write(std::string const& data);
76+ void Close();
77+
78+ void ProcessAsync();
79+
80+ static void AppendAsyncCallback(GFile* source, GAsyncResult* res, Impl* impl);
81+ static void WriteAsyncCallback(GOutputStream* source, GAsyncResult* res, Impl* impl);
82+ static void CloseAsyncCallback(GOutputStream* source, GAsyncResult* res, Impl* impl);
83+
84+ AsyncFileWriter* owner_;
85+ GCancellable* cancel_;
86+ GFile* file_;
87+ GFileOutputStream* output_stream_;
88+ bool close_pending_;
89+ bool pending_async_call_;
90+
91+ std::stringstream pending_content_;
92+ std::string data_to_write_;
93+};
94+
95+
96+AsyncFileWriter::Impl::Impl(AsyncFileWriter* owner, std::string const& filename)
97+ : owner_(owner)
98+ , cancel_(g_cancellable_new())
99+ , file_(g_file_new_for_path(filename.c_str()))
100+ , output_stream_(0)
101+ , close_pending_(false)
102+ , pending_async_call_(true)
103+{
104+ g_file_append_to_async(file_,
105+ G_FILE_CREATE_NONE,
106+ G_PRIORITY_DEFAULT,
107+ cancel_,
108+ (GAsyncReadyCallback)&AsyncFileWriter::Impl::AppendAsyncCallback,
109+ this);
110+}
111+
112+AsyncFileWriter::Impl::~Impl()
113+{
114+ if (pending_async_call_)
115+ {
116+ g_cancellable_cancel(cancel_);
117+ }
118+ // make sure the file is closed.
119+ if (output_stream_)
120+ {
121+ // If we had an output stream, sync write any pending content.
122+ if (pending_content_.tellp())
123+ {
124+ std::string data(pending_content_.str());
125+ gsize bytes_written;
126+ g_output_stream_write_all((GOutputStream*)output_stream_,
127+ data.c_str(),
128+ data.size(),
129+ &bytes_written,
130+ NULL, NULL);
131+ }
132+ owner_->closed.emit();
133+ g_object_unref(output_stream_);
134+ }
135+
136+ g_object_unref(file_);
137+ g_object_unref(cancel_);
138+}
139+
140+void AsyncFileWriter::Impl::AppendAsyncCallback(GFile* source,
141+ GAsyncResult* res,
142+ Impl* impl)
143+{
144+ GError* error = NULL;
145+ GFileOutputStream* stream = g_file_append_to_finish(source, res, &error);
146+ if (error) {
147+ // Cancelled callbacks call back, but have a cancelled error code.
148+ if (error->code != G_IO_ERROR_CANCELLED) {
149+ std::cerr << error->message << "\n";
150+ }
151+ g_error_free(error);
152+ return;
153+ }
154+ impl->output_stream_ = stream;
155+ impl->pending_async_call_ = false;
156+ impl->owner_->opened.emit();
157+ impl->ProcessAsync();
158+}
159+
160+void AsyncFileWriter::Impl::Write(std::string const& data)
161+{
162+ if (close_pending_) return;
163+ // TODO: lock the pending_content_ access
164+ pending_content_ << data;
165+ ProcessAsync();
166+}
167+
168+void AsyncFileWriter::Impl::ProcessAsync()
169+{
170+ if (output_stream_ == NULL || pending_async_call_) return;
171+
172+ if (pending_content_.tellp())
173+ {
174+ // TODO: lock the pending_content_ access
175+ data_to_write_ = pending_content_.str();
176+ g_output_stream_write_async((GOutputStream*)output_stream_,
177+ data_to_write_.c_str(),
178+ data_to_write_.size(),
179+ G_PRIORITY_DEFAULT,
180+ cancel_,
181+ (GAsyncReadyCallback)&AsyncFileWriter::Impl::WriteAsyncCallback,
182+ this);
183+ pending_async_call_ = true;
184+ }
185+ else if (close_pending_)
186+ {
187+ g_output_stream_close_async((GOutputStream*)output_stream_,
188+ G_PRIORITY_DEFAULT,
189+ cancel_,
190+ (GAsyncReadyCallback)&AsyncFileWriter::Impl::CloseAsyncCallback,
191+ this);
192+ pending_async_call_ = true;
193+ }
194+}
195+
196+void AsyncFileWriter::Impl::WriteAsyncCallback(GOutputStream* source,
197+ GAsyncResult* res,
198+ Impl* impl)
199+{
200+ GError* error = NULL;
201+ gssize g_bytes_written = g_output_stream_write_finish(source, res, &error);
202+ if (error) {
203+ // Cancelled callbacks call back, but have a cancelled error code.
204+ if (error->code != G_IO_ERROR_CANCELLED) {
205+ std::cerr << error->message << "\n";
206+ }
207+ g_error_free(error);
208+ return;
209+ }
210+ // g_bytes_written is signed from gio, but only negative if there is an error.
211+ // The error should be set too if there was an error, so no negative bytes
212+ // written get past here.
213+ std::size_t bytes_written = g_bytes_written;
214+ impl->pending_async_call_ = false;
215+ // TODO: lock the pending_content_ access
216+ std::string data = impl->pending_content_.str();
217+ if (bytes_written >= data.size()) {
218+ // There should be no reason why bytes_written should be greater than the
219+ // number of bytes in the stream, but this is paranoia.
220+ impl->pending_content_.str("");
221+ } else {
222+ impl->pending_content_.str(data.substr(bytes_written));
223+ }
224+ impl->ProcessAsync();
225+}
226+
227+void AsyncFileWriter::Impl::Close()
228+{
229+ close_pending_ = true;
230+ ProcessAsync();
231+}
232+
233+void AsyncFileWriter::Impl::CloseAsyncCallback(GOutputStream* source,
234+ GAsyncResult* res,
235+ Impl* impl)
236+{
237+ GError* error = NULL;
238+ g_output_stream_close_finish(source, res, &error);
239+ if (error) {
240+ // Cancelled callbacks call back, but have a cancelled error code.
241+ if (error->code != G_IO_ERROR_CANCELLED) {
242+ std::cerr << error->message << "\n";
243+ }
244+ g_error_free(error);
245+ return;
246+ }
247+ g_object_unref(impl->output_stream_);
248+ impl->output_stream_ = 0;
249+ impl->owner_->closed.emit();
250+}
251+
252+AsyncFileWriter::AsyncFileWriter(std::string const& filename)
253+ : pimpl(new Impl(this, filename))
254+{}
255+
256+AsyncFileWriter::~AsyncFileWriter()
257+{
258+ delete pimpl;
259+}
260+
261+void AsyncFileWriter::Write(std::string const& data)
262+{
263+ pimpl->Write(data);
264+}
265+
266+void AsyncFileWriter::Close()
267+{
268+ pimpl->Close();
269+}
270+
271+bool AsyncFileWriter::IsClosing() const
272+{
273+ return pimpl->close_pending_;
274+}
275+
276+
277+} // namespace nux
278
279=== added file 'NuxCore/AsyncFileWriter.h'
280--- NuxCore/AsyncFileWriter.h 1970-01-01 00:00:00 +0000
281+++ NuxCore/AsyncFileWriter.h 2011-07-15 04:02:32 +0000
282@@ -0,0 +1,63 @@
283+// -*- Mode: C++; indent-tabs-mode: nil; tab-width: 2 -*-
284+/*
285+ * Copyright 2011 Inalogic® Inc.
286+ *
287+ * This program is free software: you can redistribute it and/or modify it
288+ * under the terms of the GNU Lesser General Public License, as
289+ * published by the Free Software Foundation; either version 2.1 or 3.0
290+ * of the License.
291+ *
292+ * This program is distributed in the hope that it will be useful, but
293+ * WITHOUT ANY WARRANTY; without even the implied warranties of
294+ * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
295+ * PURPOSE. See the applicable version of the GNU Lesser General Public
296+ * License for more details.
297+ *
298+ * You should have received a copy of both the GNU Lesser General Public
299+ * License along with this program. If not, see <http://www.gnu.org/licenses/>
300+ *
301+ * Authored by: Tim Penhey <tim.penhey@canonical.com>
302+ *
303+ */
304+#ifndef NUXCORE_ASYNC_FILE_WRITER_H
305+#define NUXCORE_ASYNC_FILE_WRITER_H
306+
307+#include <string>
308+#include <sigc++/sigc++.h>
309+
310+namespace nux
311+{
312+
313+/**
314+ * Write to a file asynchronously.
315+ *
316+ * This uses the GIO async functions, and as such depend on the gobject main
317+ * loop.
318+ */
319+class AsyncFileWriter
320+{
321+public:
322+ AsyncFileWriter(std::string const& filename);
323+ // Destructor kills any pending async requests, and close the file
324+ // synchronously if it is open.
325+ ~AsyncFileWriter();
326+
327+ // Queue the data for writing. It'll happen some time.
328+ void Write(std::string const& data);
329+ // Close the file asynchronously. When the file is closed, the closed
330+ // signal is emitted.
331+ void Close();
332+
333+ bool IsClosing() const;
334+
335+ sigc::signal<void> opened;
336+ sigc::signal<void> closed;
337+
338+private:
339+ class Impl;
340+ Impl* pimpl;
341+};
342+
343+}
344+
345+#endif
346
347=== modified file 'NuxCore/Makefile.am'
348--- NuxCore/Makefile.am 2011-06-20 22:57:47 +0000
349+++ NuxCore/Makefile.am 2011-07-15 04:02:32 +0000
350@@ -25,6 +25,7 @@
351 $(NUX_LT_LDFLAGS)
352
353 source_cpp = \
354+ $(srcdir)/AsyncFileWriter.cpp \
355 $(srcdir)/TextString.cpp \
356 $(srcdir)/TimeFunctions.cpp \
357 $(srcdir)/Template.cpp \
358@@ -80,6 +81,7 @@
359 $(srcdir)/Math/Vector3.cpp \
360 $(srcdir)/Math/Line3D.cpp \
361 $(srcdir)/Math/Vector2.cpp \
362+ $(srcdir)/RollingFileAppender.cpp \
363 $(srcdir)/CRC32.cpp \
364 $(srcdir)/InitiallyUnownedObject.cpp
365
366@@ -126,6 +128,7 @@
367 $(srcdir)/SmartPtr/GenericSmartPointer.h
368
369 source_h = \
370+ $(srcdir)/AsyncFileWriter.h \
371 $(srcdir)/SystemTypes.h \
372 $(srcdir)/Point.h \
373 $(srcdir)/CPU.h \
374@@ -141,6 +144,7 @@
375 $(srcdir)/Colors.h \
376 $(srcdir)/Logger.h \
377 $(srcdir)/LoggingWriter.h \
378+ $(srcdir)/RollingFileAppender.h \
379 $(srcdir)/Memory.h \
380 $(srcdir)/Error.h \
381 $(srcdir)/SystemGNU.h \
382
383=== added file 'NuxCore/RollingFileAppender.cpp'
384--- NuxCore/RollingFileAppender.cpp 1970-01-01 00:00:00 +0000
385+++ NuxCore/RollingFileAppender.cpp 2011-07-15 04:02:32 +0000
386@@ -0,0 +1,231 @@
387+// -*- Mode: C++; indent-tabs-mode: nil; tab-width: 2 -*-
388+/*
389+ * Copyright 2011 Inalogic® Inc.
390+ *
391+ * This program is free software: you can redistribute it and/or modify it
392+ * under the terms of the GNU Lesser General Public License, as
393+ * published by the Free Software Foundation; either version 2.1 or 3.0
394+ * of the License.
395+ *
396+ * This program is distributed in the hope that it will be useful, but
397+ * WITHOUT ANY WARRANTY; without even the implied warranties of
398+ * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
399+ * PURPOSE. See the applicable version of the GNU Lesser General Public
400+ * License for more details.
401+ *
402+ * You should have received a copy of both the GNU Lesser General Public
403+ * License along with this program. If not, see <http://www.gnu.org/licenses/>
404+ *
405+ * Authored by: Tim Penhey <tim.penhey@canonical.com>
406+ *
407+ */
408+
409+#include "RollingFileAppender.h"
410+
411+#include <fstream>
412+#include <memory>
413+#include <sstream>
414+#include <stdexcept>
415+
416+#include <gio/gio.h>
417+
418+#include <boost/lexical_cast.hpp>
419+
420+#include "AsyncFileWriter.h"
421+
422+namespace nux {
423+namespace logging {
424+
425+namespace {
426+
427+std::string backup_path(std::string const& path, unsigned number)
428+{
429+ if (number == 0)
430+ return path;
431+
432+ std::ostringstream sout;
433+ sout << path << "." << number;
434+ return sout.str();
435+}
436+
437+// Change the implementation to string buf
438+// Have a GFile representing the file on disk
439+// g_file_append gives a GFileOutputStream (subclass of GOutputStream)
440+// g_output_stream_write_async (only have one in progress at a time)
441+// g_output_stream_flush_async (probably don't want a pending async)
442+// keep our own count of the written bytes.
443+// Tests don't have a g_object main loop, so we have another way...
444+// while (g_main_context_pending(g_main_context_get_thread_default())) {
445+// g_main_context_iteration(g_main_context_get_thread_default());
446+// }
447+class RollingFileStreamBuffer : public std::stringbuf
448+{
449+public:
450+ RollingFileStreamBuffer(std::string const& filename,
451+ unsigned number_of_backup_files,
452+ unsigned long long max_log_size,
453+ sigc::signal<void>& files_rolled);
454+ ~RollingFileStreamBuffer();
455+protected:
456+ virtual int sync();
457+private:
458+ void AsyncWriterClosed();
459+ void RotateFiles();
460+
461+ std::shared_ptr<AsyncFileWriter> writer_;
462+ GFile* log_file_;
463+ std::string filename_;
464+ unsigned number_of_backup_files_;
465+ unsigned long long max_log_size_;
466+ unsigned long long bytes_written_;
467+ sigc::connection writer_closed_;
468+ sigc::signal<void>& files_rolled_;
469+};
470+
471+RollingFileStreamBuffer::RollingFileStreamBuffer(std::string const& filename,
472+ unsigned number_of_backup_files,
473+ unsigned long long max_log_size,
474+ sigc::signal<void>& files_rolled)
475+ : filename_(filename)
476+ , number_of_backup_files_(number_of_backup_files)
477+ , max_log_size_(max_log_size)
478+ , bytes_written_(0)
479+ , files_rolled_(files_rolled)
480+{
481+ // Make sure that the filename starts with a '/' for a full path.
482+ if (filename.empty() || filename[0] != '/') {
483+ std::string error_msg = "\"" + filename + "\" is not a full path";
484+ throw std::runtime_error(error_msg.c_str());
485+ }
486+ // Looks to see if our filename exists.
487+ if (g_file_test(filename.c_str(), G_FILE_TEST_EXISTS)) {
488+ // The filename needs to be a regular file.
489+ if (!g_file_test(filename.c_str(), G_FILE_TEST_IS_REGULAR)) {
490+ std::string error_msg = filename + " is not a regular file";
491+ throw std::runtime_error(error_msg.c_str());
492+ }
493+ // Rotate the files.
494+ RotateFiles();
495+ } else {
496+ GFile* log_file = g_file_new_for_path(filename.c_str());
497+ GFile* log_dir = g_file_get_parent(log_file);
498+ if (log_dir) {
499+ g_file_make_directory_with_parents(log_dir, NULL, NULL);
500+ g_object_unref(log_dir);
501+ g_object_unref(log_file);
502+ }
503+ else {
504+ g_object_unref(log_file);
505+ std::string error_msg = "Can't get parent for " + filename;
506+ throw std::runtime_error(error_msg.c_str());
507+ }
508+ }
509+ // Now open the filename.
510+ writer_.reset(new AsyncFileWriter(filename));
511+ log_file_ = g_file_new_for_path(filename_.c_str());
512+}
513+
514+RollingFileStreamBuffer::~RollingFileStreamBuffer()
515+{
516+ // We don't want notification when the writer closes now.
517+ if (writer_closed_.connected())
518+ writer_closed_.disconnect();
519+ g_object_unref(log_file_);
520+}
521+
522+void RollingFileStreamBuffer::RotateFiles()
523+{
524+ // If we aren't keeping backups, no rolling needed.
525+ if (number_of_backup_files_ == 0)
526+ return;
527+
528+ unsigned backup = number_of_backup_files_;
529+ std::string last_log(backup_path(filename_, backup));
530+ if (g_file_test(last_log.c_str(), G_FILE_TEST_EXISTS)) {
531+ // Attempt to remove it.
532+ GFile* logfile = g_file_new_for_path(last_log.c_str());
533+ g_file_delete(logfile, NULL, NULL);
534+ g_object_unref(logfile);
535+ }
536+ // Move the previous files out.
537+ while (backup > 0) {
538+ std::string prev_log(backup_path(filename_, --backup));
539+ if (g_file_test(prev_log.c_str(), G_FILE_TEST_EXISTS)) {
540+ GFile* dest = g_file_new_for_path(last_log.c_str());
541+ GFile* src = g_file_new_for_path(prev_log.c_str());
542+ // We don't really care if there are errors for now.
543+ g_file_move(src, dest, G_FILE_COPY_NONE, NULL, NULL, NULL, NULL);
544+ g_object_unref(src);
545+ g_object_unref(dest);
546+ }
547+ last_log = prev_log;
548+ }
549+}
550+
551+void RollingFileStreamBuffer::AsyncWriterClosed()
552+{
553+ // Rotate the files and open a new file writer.
554+ RotateFiles();
555+ writer_.reset(new AsyncFileWriter(filename_));
556+ bytes_written_ = 0;
557+ // We emit the files_rolled_ here and not in the RotateFiles method as the
558+ // RotateFiles is called from the constructor, which has a reference to the
559+ // files_rolled signal from the parent stream. If this is emitted due
560+ // rotating the files in the contructor, we get a seg fault due to trying to
561+ // use the signal before it is constructed.
562+ files_rolled_.emit();
563+}
564+
565+int RollingFileStreamBuffer::sync()
566+{
567+ // If the async file writer is in the middle of closing, there is nothing we can do.
568+ if (writer_->IsClosing())
569+ return 0;
570+
571+ std::string message = str();
572+ // reset the stream
573+ str("");
574+
575+ std::size_t message_size = message.size();
576+ if (message_size > 0)
577+ {
578+ bytes_written_ += message_size;
579+ writer_->Write(message);
580+ if (bytes_written_ > max_log_size_)
581+ {
582+ // Close the writer and once it is closed, rotate the files and open a new file.
583+ writer_closed_ = writer_->closed.connect(
584+ sigc::mem_fun(this, &RollingFileStreamBuffer::AsyncWriterClosed));
585+ writer_->Close();
586+ }
587+ }
588+ return 0; // success
589+}
590+
591+} // anon namespace
592+
593+RollingFileAppender::RollingFileAppender(std::string const& filename)
594+ : std::ostream(new RollingFileStreamBuffer(filename, 5, 1e7, files_rolled))
595+{
596+}
597+
598+RollingFileAppender::RollingFileAppender(std::string const& filename,
599+ unsigned number_of_backup_files,
600+ unsigned long long max_log_size)
601+ : std::ostream(new RollingFileStreamBuffer(filename,
602+ number_of_backup_files,
603+ max_log_size,
604+ files_rolled))
605+{
606+}
607+
608+RollingFileAppender::~RollingFileAppender()
609+{
610+ rdbuf()->pubsync();
611+ std::streambuf* buff = rdbuf(0);
612+ delete buff;
613+}
614+
615+
616+} // namespace logging
617+} // namespace nux
618
619=== added file 'NuxCore/RollingFileAppender.h'
620--- NuxCore/RollingFileAppender.h 1970-01-01 00:00:00 +0000
621+++ NuxCore/RollingFileAppender.h 2011-07-15 04:02:32 +0000
622@@ -0,0 +1,48 @@
623+// -*- Mode: C++; indent-tabs-mode: nil; tab-width: 2 -*-
624+/*
625+ * Copyright 2011 Inalogic® Inc.
626+ *
627+ * This program is free software: you can redistribute it and/or modify it
628+ * under the terms of the GNU Lesser General Public License, as
629+ * published by the Free Software Foundation; either version 2.1 or 3.0
630+ * of the License.
631+ *
632+ * This program is distributed in the hope that it will be useful, but
633+ * WITHOUT ANY WARRANTY; without even the implied warranties of
634+ * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
635+ * PURPOSE. See the applicable version of the GNU Lesser General Public
636+ * License for more details.
637+ *
638+ * You should have received a copy of both the GNU Lesser General Public
639+ * License along with this program. If not, see <http://www.gnu.org/licenses/>
640+ *
641+ * Authored by: Tim Penhey <tim.penhey@canonical.com>
642+ *
643+ */
644+#ifndef NUX_CORE_ROLLING_FILE_APPENDER_H
645+#define NUX_CORE_ROLLING_FILE_APPENDER_H
646+
647+#include <ostream>
648+#include <string>
649+
650+#include <sigc++/sigc++.h>
651+
652+namespace nux {
653+namespace logging {
654+
655+class RollingFileAppender : public std::ostream
656+{
657+public:
658+ RollingFileAppender(std::string const& filename);
659+ RollingFileAppender(std::string const& filename,
660+ unsigned number_of_backup_files,
661+ unsigned long long max_log_size);
662+ ~RollingFileAppender();
663+
664+ sigc::signal<void> files_rolled;
665+};
666+
667+}
668+}
669+
670+#endif
671
672=== modified file 'configure.ac'
673--- configure.ac 2011-07-13 10:22:48 +0000
674+++ configure.ac 2011-07-15 04:02:32 +0000
675@@ -110,7 +110,7 @@
676
677 dnl ===========================================================================
678
679-PKG_CHECK_MODULES(NUX_CORE, glib-2.0 >= 2.25.14 gthread-2.0 sigc++-2.0)
680+PKG_CHECK_MODULES(NUX_CORE, glib-2.0 >= 2.25.14 gthread-2.0 sigc++-2.0 gio-2.0)
681 AC_SUBST(NUX_CORE_CFLAGS)
682 AC_SUBST(NUX_CORE_LIBS)
683
684
685=== added file 'tests/Helpers.cpp'
686--- tests/Helpers.cpp 1970-01-01 00:00:00 +0000
687+++ tests/Helpers.cpp 2011-07-15 04:02:32 +0000
688@@ -0,0 +1,52 @@
689+// -*- Mode: C++; indent-tabs-mode: nil; tab-width: 2 -*-
690+/*
691+ * Copyright 2011 Inalogic® Inc.
692+ *
693+ * This program is free software: you can redistribute it and/or modify it
694+ * under the terms of the GNU Lesser General Public License, as
695+ * published by the Free Software Foundation; either version 2.1 or 3.0
696+ * of the License.
697+ *
698+ * This program is distributed in the hope that it will be useful, but
699+ * WITHOUT ANY WARRANTY; without even the implied warranties of
700+ * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
701+ * PURPOSE. See the applicable version of the GNU Lesser General Public
702+ * License for more details.
703+ *
704+ * You should have received a copy of both the GNU Lesser General Public
705+ * License along with this program. If not, see <http://www.gnu.org/licenses/>
706+ *
707+ * Authored by: Tim Penhey <tim.penhey@canonical.com>
708+ *
709+ */
710+
711+#include "Helpers.h"
712+
713+#include <fstream>
714+#include <stdexcept>
715+
716+namespace nux
717+{
718+namespace testing
719+{
720+
721+std::string ReadFile(std::string const& filename)
722+{
723+ std::ifstream input(filename.c_str());
724+ if (input.bad())
725+ throw std::runtime_error("bad file");
726+ return std::string((std::istreambuf_iterator<char>(input)),
727+ std::istreambuf_iterator<char>());
728+}
729+
730+void PumpGObjectMainLoop()
731+{
732+ GMainContext* context(g_main_context_get_thread_default());
733+ while (g_main_context_pending(context)) {
734+ g_main_context_iteration(context, false);
735+ }
736+}
737+
738+
739+}
740+}
741
742=== added file 'tests/Helpers.h'
743--- tests/Helpers.h 1970-01-01 00:00:00 +0000
744+++ tests/Helpers.h 2011-07-15 04:02:32 +0000
745@@ -0,0 +1,63 @@
746+// -*- Mode: C++; indent-tabs-mode: nil; tab-width: 2 -*-
747+/*
748+ * Copyright 2011 Inalogic® Inc.
749+ *
750+ * This program is free software: you can redistribute it and/or modify it
751+ * under the terms of the GNU Lesser General Public License, as
752+ * published by the Free Software Foundation; either version 2.1 or 3.0
753+ * of the License.
754+ *
755+ * This program is distributed in the hope that it will be useful, but
756+ * WITHOUT ANY WARRANTY; without even the implied warranties of
757+ * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
758+ * PURPOSE. See the applicable version of the GNU Lesser General Public
759+ * License for more details.
760+ *
761+ * You should have received a copy of both the GNU Lesser General Public
762+ * License along with this program. If not, see <http://www.gnu.org/licenses/>
763+ *
764+ * Authored by: Tim Penhey <tim.penhey@canonical.com>
765+ *
766+ */
767+#ifndef NUX_TESTS_FILE_HELPERS_H
768+#define NUX_TESTS_FILE_HELPERS_H
769+
770+#include <string>
771+#include <glib.h>
772+#include <sigc++/sigc++.h>
773+
774+
775+namespace nux
776+{
777+namespace testing
778+{
779+
780+std::string ReadFile(std::string const& filename);
781+void PumpGObjectMainLoop();
782+
783+class TestCallback
784+{
785+public:
786+ TestCallback() : happened(false) {}
787+
788+ sigc::slot<void> sigc_callback() {
789+ return sigc::mem_fun(this, &TestCallback::callback);
790+ }
791+
792+ static gboolean glib_callback(gpointer data) {
793+ TestCallback* test = reinterpret_cast<TestCallback*>(data);
794+ test->callback();
795+ return FALSE;
796+ }
797+ void callback() {
798+ happened = true;
799+ }
800+
801+ bool happened;
802+};
803+
804+
805+}
806+}
807+
808+#endif
809
810=== modified file 'tests/Makefile.am'
811--- tests/Makefile.am 2011-06-24 14:49:09 +0000
812+++ tests/Makefile.am 2011-07-15 04:02:32 +0000
813@@ -33,8 +33,12 @@
814 $(NUX_LIBS)
815
816 gtest_nux_core_SOURCES = \
817+ Helpers.cpp \
818+ test_main.cpp \
819+ test_async_file_writer.cpp \
820 test_logger.cpp \
821- test_properties.cpp
822+ test_properties.cpp \
823+ test_rolling_file_appender.cpp
824
825 gtest_nux_core_CPPFLAGS = \
826 -I$(srcdir) \
827@@ -55,7 +59,8 @@
828 $(NUX_LIBS)
829
830 gtest_nux_core_LDFLAGS = \
831- -lpthread -lgtest -lgtest_main -lgmock
832+ -lpthread -lgtest -lgmock \
833+ -lboost_filesystem -lboost_system
834
835
836 #run make test as part of make check
837
838=== added file 'tests/test_async_file_writer.cpp'
839--- tests/test_async_file_writer.cpp 1970-01-01 00:00:00 +0000
840+++ tests/test_async_file_writer.cpp 2011-07-15 04:02:32 +0000
841@@ -0,0 +1,110 @@
842+#include <string>
843+#include <fstream>
844+
845+#include <iostream>
846+
847+#include <gmock/gmock.h>
848+
849+#include <boost/filesystem.hpp>
850+
851+#include <glib.h>
852+
853+#include "NuxCore/AsyncFileWriter.h"
854+
855+#include "Helpers.h"
856+
857+namespace bf = boost::filesystem;
858+using namespace testing;
859+using namespace nux::testing;
860+
861+namespace {
862+
863+const std::string TEST_ROOT("/tmp/nux-test-cases");
864+
865+
866+class TestAsyncfileWriter : public ::testing::Test
867+{
868+protected:
869+ virtual void SetUp() {
870+ // Make sure that the tests start with and empty TEST_ROOT.
871+ bf::remove_all(TEST_ROOT);
872+ bf::create_directories(TEST_ROOT);
873+ }
874+
875+ virtual void TearDown() {
876+ // Delete the unity test directory
877+ bf::remove_all(TEST_ROOT);
878+ }
879+
880+ bool WaitForOpen(nux::AsyncFileWriter& writer, unsigned timeout = 5) {
881+ TestCallback opened;
882+ TestCallback timed_out;
883+ g_timeout_add_seconds(timeout, &TestCallback::glib_callback, &timed_out);
884+ writer.opened.connect(opened.sigc_callback());
885+
886+ while (!opened.happened && !timed_out.happened) {
887+ PumpGObjectMainLoop();
888+ }
889+ return opened.happened;
890+ }
891+
892+ bool WaitForClose(nux::AsyncFileWriter& writer, unsigned timeout = 5) {
893+ TestCallback closed;
894+ TestCallback timed_out;
895+ g_timeout_add_seconds(timeout, &TestCallback::glib_callback, &timed_out);
896+ writer.closed.connect(closed.sigc_callback());
897+
898+ while (!closed.happened && !timed_out.happened) {
899+ PumpGObjectMainLoop();
900+ }
901+ return closed.happened;
902+ }
903+
904+};
905+
906+TEST_F(TestAsyncfileWriter, TestConstructor) {
907+ std::string filename(TEST_ROOT + "/empty-file");
908+ {
909+ nux::AsyncFileWriter writer(filename);
910+ bool opened = WaitForOpen(writer);
911+ EXPECT_TRUE(opened);
912+ }
913+ EXPECT_TRUE(bf::exists(filename));
914+ EXPECT_THAT(ReadFile(filename), Eq(""));
915+}
916+
917+TEST_F(TestAsyncfileWriter, TestWrites) {
918+ std::string filename(TEST_ROOT + "/write-file");
919+ std::string data(200, 'x');
920+ {
921+ nux::AsyncFileWriter writer(filename);
922+ writer.Write(data);
923+ writer.Close();
924+ bool closed = WaitForClose(writer);
925+ EXPECT_TRUE(closed);
926+ }
927+ EXPECT_THAT(ReadFile(filename), Eq(data));
928+}
929+
930+TEST_F(TestAsyncfileWriter, TestWriteLots) {
931+ std::string filename(TEST_ROOT + "/lots-file");
932+ std::string data(200, 'x');
933+ const int loop_count = 1000;
934+ {
935+ nux::AsyncFileWriter writer(filename);
936+ for (int i = 0; i < loop_count; ++i) {
937+ writer.Write(data);
938+ }
939+ writer.Close();
940+ bool closed = WaitForClose(writer);
941+ EXPECT_TRUE(closed);
942+ }
943+ std::string file_content = ReadFile(filename);
944+ EXPECT_THAT(file_content.size(), Eq(data.size() * loop_count));
945+ // They are all x's.
946+ EXPECT_THAT(file_content, MatchesRegex("^x+$"));
947+}
948+
949+
950+
951+} // anon namespace
952
953=== added file 'tests/test_main.cpp'
954--- tests/test_main.cpp 1970-01-01 00:00:00 +0000
955+++ tests/test_main.cpp 2011-07-15 04:02:32 +0000
956@@ -0,0 +1,10 @@
957+#include <gtest/gtest.h>
958+#include <glib-object.h>
959+
960+int main(int argc, char **argv)
961+{
962+ ::testing::InitGoogleTest(&argc, argv);
963+ g_type_init();
964+
965+ return RUN_ALL_TESTS();
966+}
967
968=== added file 'tests/test_rolling_file_appender.cpp'
969--- tests/test_rolling_file_appender.cpp 1970-01-01 00:00:00 +0000
970+++ tests/test_rolling_file_appender.cpp 2011-07-15 04:02:32 +0000
971@@ -0,0 +1,147 @@
972+#include <gtest/gtest.h>
973+#include <gmock/gmock.h>
974+
975+#include <fstream>
976+#include <streambuf>
977+#include <string>
978+
979+#include <boost/filesystem.hpp>
980+#include <boost/filesystem/fstream.hpp>
981+
982+#include "NuxCore/RollingFileAppender.h"
983+#include "Helpers.h"
984+
985+namespace bf = boost::filesystem;
986+
987+using namespace nux::logging;
988+using namespace nux::testing;
989+using namespace testing;
990+
991+namespace {
992+
993+/**
994+ * Due to the asynchronous manner in which the rolling file appender writes
995+ * the data to disk, it is incredibly hard to test in a simple unit test as
996+ * the underlying file on the file system isn't created synchronously, nor is
997+ * the output written synchronously.
998+ *
999+ * A files_rolled event exists on the RollingFileAppender so we can at least
1000+ * wait for that signal and check the underlying files on the file system at
1001+ * that stage. This does mean that we aren't testing the smaller chunks of
1002+ * how it works, but more only the complete system, which I guess has to be
1003+ * good enough for this case.
1004+ */
1005+
1006+const std::string TEST_ROOT("/tmp/nux-test-cases");
1007+
1008+class TestRollingFileAppender : public ::testing::Test
1009+{
1010+protected:
1011+ virtual void SetUp() {
1012+ // Make sure that the tests start with nothing there.
1013+ bf::remove_all(TEST_ROOT);
1014+ }
1015+
1016+ virtual void TearDown() {
1017+ // Delete the unity test directory
1018+ bf::remove_all(TEST_ROOT);
1019+ }
1020+
1021+ bool WaitForRoll(RollingFileAppender& appender, unsigned timeout = 5) {
1022+ TestCallback rolled;
1023+ TestCallback timed_out;
1024+ g_timeout_add_seconds(timeout, &TestCallback::glib_callback, &timed_out);
1025+ appender.files_rolled.connect(rolled.sigc_callback());
1026+
1027+ while (!rolled.happened && !timed_out.happened) {
1028+ PumpGObjectMainLoop();
1029+ }
1030+ return rolled.happened;
1031+ }
1032+
1033+};
1034+
1035+TEST_F(TestRollingFileAppender, NoTestRoot) {
1036+ // The test root should not exist.
1037+ EXPECT_FALSE(bf::exists(TEST_ROOT));
1038+}
1039+
1040+TEST_F(TestRollingFileAppender, TestLogFileRollsAtFlush) {
1041+
1042+ std::string logfile = TEST_ROOT + "/nux.log";
1043+ unsigned max_log_size = 20; // roll every 20 characters
1044+ RollingFileAppender output(logfile, 5, max_log_size);
1045+
1046+ output << "Testing the rolling of the logfile" << std::endl;
1047+ WaitForRoll(output);
1048+ output << "Long line greater than max_log_size" << std::endl;
1049+ WaitForRoll(output);
1050+
1051+ // Since the log files are rolled on flush, if the last thing written out
1052+ // takes the filesize greater than the max_log_size, the log files are
1053+ // rolled and the current file being appended to is now empty.
1054+ EXPECT_THAT(ReadFile(logfile + ".1"),
1055+ Eq("Long line greater than max_log_size\n"));
1056+ EXPECT_THAT(ReadFile(logfile + ".2"),
1057+ Eq("Testing the rolling of the logfile\n"));
1058+}
1059+
1060+TEST_F(TestRollingFileAppender, TestExistingLogFileMoved) {
1061+
1062+ std::string logfile = TEST_ROOT + "/nux.log";
1063+ {
1064+ bf::create_directories(bf::path(logfile).parent_path());
1065+ std::ofstream output(logfile);
1066+ output << "Existing file.";
1067+ }
1068+ EXPECT_TRUE(bf::exists(logfile));
1069+
1070+ RollingFileAppender output(logfile);
1071+
1072+ EXPECT_THAT(ReadFile(logfile + ".1"),
1073+ Eq("Existing file."));
1074+}
1075+
1076+TEST_F(TestRollingFileAppender, TestDeletingOld) {
1077+
1078+ std::string logfile = TEST_ROOT + "/nux.log";
1079+ // Two backups, size 20 bytes.
1080+ RollingFileAppender output(logfile, 2, 20);
1081+
1082+ // Due to the asynchronous manner in which the output is sent to the
1083+ // underlying file, we explicitly wait for the roll here. Otherwise we may
1084+ // just send all the logging lines to one file then it would roll.
1085+ output << "Oldest line should be deleted." << std::endl;
1086+ WaitForRoll(output);
1087+ output << "This line will be in the last backup." << std::endl;
1088+ WaitForRoll(output);
1089+ output << "This is backup number 1." << std::endl;
1090+ WaitForRoll(output);
1091+
1092+ EXPECT_THAT(ReadFile(logfile + ".1"),
1093+ Eq("This is backup number 1.\n"));
1094+ EXPECT_THAT(ReadFile(logfile + ".2"),
1095+ Eq("This line will be in the last backup.\n"));
1096+ EXPECT_FALSE(bf::exists(logfile + ".3"));
1097+}
1098+
1099+TEST_F(TestRollingFileAppender, TestFullPathNeeded) {
1100+ EXPECT_THROW(RollingFileAppender("nux.log"), std::runtime_error);
1101+ EXPECT_THROW(RollingFileAppender("relative/nux.log"), std::runtime_error);
1102+}
1103+
1104+TEST_F(TestRollingFileAppender, TestFileNeeded) {
1105+ // For some obscure reason, EXPECT_THROW won't accept:
1106+ // RollingFileAppender(logfile)
1107+ // as its first arg.
1108+ std::string directory_path = TEST_ROOT + "/somedir";
1109+ bf::create_directories(directory_path);
1110+ EXPECT_THROW(RollingFileAppender appender(directory_path), std::runtime_error);
1111+
1112+ std::string symlink_path = TEST_ROOT + "/somelink";
1113+ bf::create_symlink(directory_path, symlink_path);
1114+ EXPECT_THROW(RollingFileAppender appender(symlink_path), std::runtime_error);
1115+}
1116+
1117+
1118+} // anon namespace

Subscribers

People subscribed via source and target branches