radiant/console: make logging thread-safe
authorAntoine Fontaine <antoine.fontaine@epfl.ch>
Mon, 22 Mar 2021 01:10:47 +0000 (02:10 +0100)
committerAntoine Fontaine <antoine.fontaine@epfl.ch>
Wed, 24 Mar 2021 19:31:35 +0000 (20:31 +0100)
radiant/console.cpp
radiant/console.h

index e6479cb1a1e7012c5d230684827a681b699f695c..290e4429e5bda37e742a4e67f1eaa7fa441b02df 100644 (file)
@@ -24,6 +24,7 @@
 #include <time.h>
 #include <uilib/uilib.h>
 #include <gtk/gtk.h>
+#include <mutex>
 
 #include "gtkutil/accelerator.h"
 #include "gtkutil/messagebox.h"
@@ -45,6 +46,13 @@ FILE* g_hLogFile;
 
 bool g_Console_enableLogging = false;
 
+struct Gtk_Idle_Print_Data {
+       int level;
+       char *buf;
+       std::size_t length;
+       bool contains_newline;
+};
+
 // called whenever we need to open/close/check the console log file
 void Sys_LogFile( bool enable ){
        if ( enable && !g_hLogFile ) {
@@ -146,6 +154,69 @@ std::size_t write( const char* buffer, std::size_t length ){
 }
 };
 
+// This function is meant to be used with gtk_idle_add. It will free its argument.
+static gboolean Gtk_Idle_Print( gpointer data ){
+       Gtk_Idle_Print_Data *args = reinterpret_cast<Gtk_Idle_Print_Data *>(data);
+       g_assert(g_console);
+
+       auto buffer = gtk_text_view_get_buffer( g_console );
+
+       GtkTextIter iter;
+       gtk_text_buffer_get_end_iter( buffer, &iter );
+
+       static auto end = gtk_text_buffer_create_mark( buffer, "end", &iter, FALSE );
+
+       const GdkColor yellow = { 0, 0xb0ff, 0xb0ff, 0x0000 };
+       const GdkColor red = { 0, 0xffff, 0x0000, 0x0000 };
+
+       static auto error_tag = gtk_text_buffer_create_tag( buffer, "red_foreground", "foreground-gdk", &red, NULL );
+       static auto warning_tag = gtk_text_buffer_create_tag( buffer, "yellow_foreground", "foreground-gdk", &yellow, NULL );
+       static auto standard_tag = gtk_text_buffer_create_tag( buffer, "black_foreground", NULL );
+       GtkTextTag* tag;
+       switch ( args->level )
+       {
+       case SYS_WRN:
+               tag = warning_tag;
+               break;
+       case SYS_ERR:
+               tag = error_tag;
+               break;
+       case SYS_STD:
+       case SYS_VRB:
+       default:
+               tag = standard_tag;
+               break;
+       }
+
+       {
+               GtkTextBufferOutputStream textBuffer( buffer, &iter, tag );
+               if ( !globalCharacterSet().isUTF8() ) {
+                       BufferedTextOutputStream<GtkTextBufferOutputStream> buffered( textBuffer );
+                       buffered << StringRange( args->buf, args->buf + args->length );
+               }
+               else
+               {
+                       textBuffer << StringRange( args->buf, args->buf + args->length );
+               }
+       }
+
+       // update console widget immediatly if we're doing something time-consuming
+       if ( args->contains_newline ) {
+               gtk_text_view_scroll_mark_onscreen( g_console, end );
+
+               if ( !ScreenUpdates_Enabled() && gtk_widget_get_realized( g_console ) ) {
+                       ScreenUpdates_process();
+               }
+       }
+
+       free( args->buf );
+       free( args );
+
+       return FALSE; // call this once, not repeatedly
+}
+
+// Print logs to the in-game console and/or to the log file.
+// This function is thread safe.
 std::size_t Sys_Print( int level, const char* buf, std::size_t length ){
        bool contains_newline = std::find( buf, buf + length, '\n' ) != buf + length;
 
@@ -154,66 +225,24 @@ std::size_t Sys_Print( int level, const char* buf, std::size_t length ){
        }
 
        if ( g_hLogFile != 0 ) {
+               // prevent parallel write
+               static std::mutex log_file_mutex;
+               std::lock_guard<std::mutex> guard(log_file_mutex);
+
                fwrite( buf, 1, length, g_hLogFile );
                if ( contains_newline ) {
                        fflush( g_hLogFile );
                }
        }
 
-       if ( level != SYS_NOCON ) {
-               if ( g_console ) {
-                       auto buffer = gtk_text_view_get_buffer( g_console );
-
-                       GtkTextIter iter;
-                       gtk_text_buffer_get_end_iter( buffer, &iter );
-
-                       static auto end = gtk_text_buffer_create_mark( buffer, "end", &iter, FALSE );
-
-                       const GdkColor yellow = { 0, 0xb0ff, 0xb0ff, 0x0000 };
-                       const GdkColor red = { 0, 0xffff, 0x0000, 0x0000 };
-
-                       static auto error_tag = gtk_text_buffer_create_tag( buffer, "red_foreground", "foreground-gdk", &red, NULL );
-                       static auto warning_tag = gtk_text_buffer_create_tag( buffer, "yellow_foreground", "foreground-gdk", &yellow, NULL );
-                       static auto standard_tag = gtk_text_buffer_create_tag( buffer, "black_foreground", NULL );
-                       GtkTextTag* tag;
-                       switch ( level )
-                       {
-                       case SYS_WRN:
-                               tag = warning_tag;
-                               break;
-                       case SYS_ERR:
-                               tag = error_tag;
-                               break;
-                       case SYS_STD:
-                       case SYS_VRB:
-                       default:
-                               tag = standard_tag;
-                               break;
-                       }
-
-
-                       {
-                               GtkTextBufferOutputStream textBuffer( buffer, &iter, tag );
-                               if ( !globalCharacterSet().isUTF8() ) {
-                                       BufferedTextOutputStream<GtkTextBufferOutputStream> buffered( textBuffer );
-                                       buffered << StringRange( buf, buf + length );
-                               }
-                               else
-                               {
-                                       textBuffer << StringRange( buf, buf + length );
-                               }
-                       }
-
-                       // update console widget immediatly if we're doing something time-consuming
-                       if ( contains_newline ) {
-                               gtk_text_view_scroll_mark_onscreen( g_console, end );
-
-                               if ( !ScreenUpdates_Enabled() && gtk_widget_get_realized( g_console ) ) {
-                                       ScreenUpdates_process();
-                               }
-                       }
+       if ( level != SYS_NOCON && g_console ) {
+               auto data = reinterpret_cast<Gtk_Idle_Print_Data *>( malloc( sizeof(struct Gtk_Idle_Print_Data) ) );
+               if (data != nullptr) {
+                       *data = { level, g_strndup(buf, length), length, contains_newline };
+                       gdk_threads_add_idle(Gtk_Idle_Print, (gpointer)data);
                }
        }
+
        return length;
 }
 
index 8bbe71e1c512ab05777f8e64de01a444892c7b49..44d63f103605aee0cd4967db62a1d3e93a3cfb99 100644 (file)
 #define SYS_ERR 3 ///< error
 #define SYS_NOCON 4 ///< no console, only print to the file (useful whenever Sys_Printf and output IS the problem)
 
-std::size_t Sys_Print( int level, const char* buf, std::size_t length );
 class TextOutputStream;
 TextOutputStream& getSysPrintOutputStream();
 TextOutputStream& getSysPrintErrorStream();
 
 ui::Widget Console_constructWindow( ui::Window toplevel );
 
+std::size_t Sys_Print( int level, const char* buf, std::size_t length );
+
 // will open/close the log file based on the parameter
 void Sys_LogFile( bool enable );
 extern bool g_Console_enableLogging;