/*
* dest_file.c - syslogd implementation for windows, file destination
*
* Created by Alexander Yaworsky
*
* Asynchronous i/o is not supported under win9x so we have to use
* a separate thread for each file.
*
* THIS SOFTWARE IS NOT COPYRIGHTED
*
* This source code is offered for use in the public domain. You may
* use, modify or distribute it freely.
*
* This code is distributed in the hope that it will be useful but
* WITHOUT ANY WARRANTY. ALL WARRANTIES, EXPRESS OR IMPLIED ARE HEREBY
* DISCLAIMED. This includes but is not limited to warranties of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
*/
#include <ctype.h>
#include <errno.h>
#include <process.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <windows.h>
#include <glib.h>
#include <syslog.h>
#include <syslogd.h>
#define WRITER_KEEPALIVE_TIME 10000
struct file_writer
{
struct destination *destination;
gchar *file_name;
struct fifo *message_queue;
CRITICAL_SECTION fifo_cs;
HANDLE fifo_event; /* manual-reset event */
HANDLE shutdown_event; /* manual-reset event */
HANDLE fd;
struct message *first_msg, *second_msg, *current_msg;
int coalesce_count;
time_t max_hold;
};
struct dest_extra
{
GList *file_writers;
CRITICAL_SECTION cs_file_writers;
};
/* forward declarations */
static unsigned __stdcall writer_thread_proc( void* arg );
/******************************************************************************
* compare_current_and_first_messages
*
* Auxiliary function for coalescer.
* If messages aren't identical, reset coalescer.
*/
static gboolean compare_current_and_first_messages( struct file_writer* writer )
{
TRACE_ENTER( "%p\n", writer );
if( string_equal( writer->current_msg->hostname,
writer->first_msg->hostname )
&& string_equal( writer->current_msg->message,
writer->first_msg->message ) )
{
TRACE_LEAVE( "%p ok\n" );
return TRUE;
}
release_message( writer->first_msg );
writer->first_msg = NULL;
TRACE_LEAVE( "%p done; messages aren't identical\n", writer );
return FALSE;
}
/******************************************************************************
* init_coalescer
*
* Should be called when we've got the first unique message.
* Save message for subsequent comparsions and set max hold time.
*/
static void init_coalescer( struct file_writer* writer )
{
writer->first_msg = writer->current_msg;
reference_message( writer->first_msg );
writer->max_hold = time(NULL) + hold;
writer->coalesce_count = 1;
TRACE_2( "%p max_hold=%ld\n", writer, writer->max_hold );
}
/******************************************************************************
* coalesce
*
* coalesce messages;
* If there are more than two sequential identical messages then we
* should write first of them followed by message with the number of
* subsequent messages.
*
* The caller always must process current_msg unless it is set NULL
* when coalesced;
* return: TRUE if messages are coalesced; FALSE if flushing is required
*/
static gboolean coalesce( struct file_writer* writer )
{
time_t current_time;
TRACE_ENTER( "%p\n", writer );
switch( writer->coalesce_count )
{
case 0:
/* the first message */
init_coalescer( writer );
return TRUE;
case 1:
/* the second message */
TRACE_2( "%p second message\n", writer );
if( !compare_current_and_first_messages( writer ) )
return FALSE;
writer->second_msg = writer->current_msg;
writer->current_msg = NULL;
writer->coalesce_count = 2;
goto check_hold_time;
case 2:
/* the third message */
TRACE_2( "%p third message\n", writer );
if( !compare_current_and_first_messages( writer ) )
/* leave the second message; it will be written by flush_coalescer */
return FALSE;
release_message( writer->second_msg );
writer->second_msg = NULL;
release_message( writer->current_msg );
writer->current_msg = NULL;
writer->coalesce_count = 3;
goto check_hold_time;
default:
/* the fourth and subsequent messages */
TRACE_2( "%p fourth+ message\n", writer );
if( !compare_current_and_first_messages( writer ) )
return FALSE;
release_message( writer->current_msg );
writer->current_msg = NULL;
writer->coalesce_count++;
TRACE_2( "%p coalesce_count=%d\n", writer, writer->coalesce_count );
goto check_hold_time;
}
check_hold_time:
current_time = time(NULL);
TRACE_2( "%p current_time=%ld\n", writer, current_time );
if( writer->max_hold < current_time )
{
TRACE_LEAVE( "%p done; elapsed hold time\n", writer );
return FALSE;
}
TRACE_LEAVE( "%p done\n", writer );
return TRUE;
}
/******************************************************************************
* write_message_to_logfile
*/
static void write_message_to_logfile( struct file_writer* writer, struct message** msg )
{
gchar *buffer;
gsize len;
DWORD written;
if( INVALID_HANDLE_VALUE == writer->fd )
return;
TRACE_2( "%p: %s\n", writer, (*msg)->message->gstr->str );
len = string_concat( &buffer, (*msg)->timestamp, space, (*msg)->hostname,
space, (*msg)->message, line_feed, NULL );
WriteFile( writer->fd, buffer, len, &written, NULL );
g_free( buffer );
release_message( *msg );
*msg = NULL;
}
/******************************************************************************
* flush_coalescer
*/
static void flush_coalescer( struct file_writer* writer )
{
if( writer->second_msg )
{
write_message_to_logfile( writer, &writer->second_msg );
TRACE_2( "%p written second message\n", writer );
}
if( writer->coalesce_count > 2 )
{
SYSTEMTIME stm;
char buffer[ 1024 ];
int size;
DWORD written;
GetLocalTime( &stm );
/* make informational message */
size = snprintf( buffer, sizeof(buffer),
"%s %2d %02d:%02d:%02d %s syslog: last message repeated %d times\n",
str_month[ stm.wMonth - 1 ], stm.wDay, stm.wHour, stm.wMinute, stm.wSecond,
local_hostname->gstr->str,
writer->coalesce_count - 1 );
WriteFile( writer->fd, buffer, size, &written, NULL );
TRACE_2( "%p made informational message\n", writer );
}
writer->coalesce_count = 0;
if( writer->first_msg )
{
release_message( writer->first_msg );
writer->first_msg = NULL;
}
if( writer->current_msg )
{
/* we just got the first message and should initialize coalescer */
init_coalescer( writer );
write_message_to_logfile( writer, &writer->current_msg );
TRACE_2( "%p written current message\n", writer );
}
}
/******************************************************************************
* destroy_file_writer
*
* destination's critical section cs_file_writers must be held when calling
* this function
*/
static void destroy_file_writer( struct file_writer* writer )
{
TRACE_ENTER( "%p\n", writer );
if( writer->destination )
{
struct dest_extra *extra = writer->destination->extra;
extra->file_writers = g_list_remove( extra->file_writers, writer );
}
if( writer->fifo_event ) CloseHandle( writer->fifo_event );
if( writer->shutdown_event ) CloseHandle( writer->shutdown_event );
DeleteCriticalSection( &writer->fifo_cs );
fifo_destroy( writer->message_queue );
g_free( writer->file_name );
g_free( writer );
TRACE_LEAVE( "done\n" );
}
/******************************************************************************
* create_file_writer
*
* destination's critical section cs_file_writers must be held when calling
* this function
*/
static struct file_writer* create_file_writer( gchar* file_name,
struct destination* destination )
{
struct file_writer *ret;
unsigned writer_thread_id;
HANDLE *writer_thread;
struct dest_extra *extra;
TRACE_ENTER( "file_name=%s\n", file_name );
ret = g_malloc0( sizeof(struct file_writer) );
ret->file_name = g_strdup( file_name );
ret->message_queue = fifo_create();
ret->fifo_event = CreateEvent( NULL, TRUE, FALSE, NULL );
if( !ret->fifo_event )
{
ERR( "Cannot create event; error %lu\n", GetLastError() );
goto error;
}
ret->shutdown_event = CreateEvent( NULL, TRUE, FALSE, NULL );
if( !ret->shutdown_event )
{
ERR( "Cannot create event; error %lu\n", GetLastError() );
goto error;
}
writer_thread = (HANDLE) _beginthreadex( NULL, 0, writer_thread_proc, ret,
CREATE_SUSPENDED, &writer_thread_id );
if( !writer_thread )
{
ERR( "Cannot create thread; error %lu\n", GetLastError() );
goto error;
}
InitializeCriticalSection( &ret->fifo_cs );
/* add writer to destination */
extra = destination->extra;
extra->file_writers = g_list_append( extra->file_writers, ret );
ret->destination = destination;
ResumeThread( writer_thread );
CloseHandle( writer_thread );
TRACE_LEAVE( "done; ret=%p\n", ret );
return ret;
error:
destroy_file_writer( ret );
TRACE_LEAVE( "error\n" );
return NULL;
}
/******************************************************************************
* pop_messages_from_queue
*
* Helper function for writer_thread_proc.
* Extract messages from queue and write them to file.
*/
static void pop_messages_from_queue( struct file_writer* writer )
{
EnterCriticalSection( &writer->fifo_cs );
for(;;)
{
writer->current_msg = fifo_pop( writer->message_queue );
if( !writer->current_msg )
break;
if( coalesce( writer ) )
{
if( writer->current_msg )
{
write_message_to_logfile( writer, &writer->current_msg );
TRACE_2( "%p written current message\n", writer );
}
}
else
{
flush_coalescer( writer );
}
}
ResetEvent( writer->fifo_event );
LeaveCriticalSection( &writer->fifo_cs );
}
/******************************************************************************
* writer_thread_proc
*
* Open file, extract messages from queue and write them to file.
* If queue is empty long enough, close file and destroy itself.
*/
static unsigned __stdcall writer_thread_proc( void* arg )
{
struct file_writer *writer = arg;
struct dest_extra *extra = writer->destination->extra;
HANDLE wait_objects[2] = { writer->fifo_event, writer->shutdown_event };
gchar *pathname;
gboolean inside_cs = FALSE;
TRACE_ENTER( "writer=%p\n", writer );
pathname = make_absolute_log_pathname( writer->file_name );
create_directories( pathname );
rotate_logfile( pathname, writer->destination );
writer->fd = CreateFile( pathname, GENERIC_WRITE, FILE_SHARE_READ, NULL,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if( INVALID_HANDLE_VALUE == writer->fd )
{
ERR( "CreateFile(%s) error %lu\n", pathname, GetLastError() );
g_free( pathname );
goto done;
}
/* there's a strange bug or feature in windows: if there was a file with the same
name in the directory, a new file will inherit its creation time;
because of this logs will be rotated each time;
here is a workaround:
*/
if( GetLastError() != ERROR_ALREADY_EXISTS )
{
FILETIME systime;
GetSystemTimeAsFileTime( &systime );
if( !SetFileTime( writer->fd, &systime, &systime, &systime ) )
ERR( "SetFileTime error %lu\n", GetLastError() );
}
g_free( pathname );
SetFilePointer( writer->fd, 0, NULL, FILE_END );
for(;;)
{
switch( WaitForMultipleObjects( 2, wait_objects, FALSE, WRITER_KEEPALIVE_TIME ) )
{
case WAIT_TIMEOUT:
case WAIT_OBJECT_0 + 1:
/* no new messages should be put into the queue until we close the file */
EnterCriticalSection( &extra->cs_file_writers );
inside_cs = TRUE;
goto done;
case WAIT_OBJECT_0:
pop_messages_from_queue( writer );
break;
}
}
done:
pop_messages_from_queue( writer );
flush_coalescer( writer );
if( writer->fd != INVALID_HANDLE_VALUE ) CloseHandle( writer->fd );
destroy_file_writer( writer );
if( inside_cs )
LeaveCriticalSection( &extra->cs_file_writers );
purge_log_dirs();
TRACE_LEAVE( "done\n" );
return 0;
}
/******************************************************************************
* make_file_name
*
* expand filename pattern (message->file)
*/
static void make_file_name( char* pattern, struct message* message, char* buffer )
{
char *dest = buffer;
SYSTEMTIME stm;
GetLocalTime( &stm );
for(;;)
{
char c = *pattern++;
if( c != '%' )
{
*dest++ = c;
if( '\0' == c )
break;
continue;
}
c = *pattern++;
switch( c )
{
case 'Y': dest += sprintf( dest, "%u", stm.wYear ); break;
case 'M': dest += sprintf( dest, "%02u", stm.wMonth ); break;
case 'm': dest += sprintf( dest, "%u", stm.wMonth ); break;
case 'D': dest += sprintf( dest, "%02u", stm.wDay ); break;
case 'd': dest += sprintf( dest, "%u", stm.wDay ); break;
case 'W': dest += sprintf( dest, "%u", stm.wDayOfWeek + 1 ); break;
case 'F': dest += sprintf( dest, "%s", get_facility_name( message->facility ) ); break;
case 'f': dest += sprintf( dest, "%d", message->facility ); break;
case 'L': dest += sprintf( dest, "%s", get_priority_name( message->priority ) ); break;
case 'l': dest += sprintf( dest, "%d", message->priority ); break;
case 'H': dest += sprintf( dest, "%s", message->hostname->gstr->str ); break;
case 'h': dest += sprintf( dest, "%s", message->sender->gstr->str ); break;
case 'P': dest += sprintf( dest, "%s", message->program->gstr->str ); break;
default: *dest++ = c; break;
}
}
strlwr( buffer );
}
/******************************************************************************
* put_message_to_file_dest
*/
static void put_message_to_file_dest( struct destination* destination, struct message* msg )
{
struct dest_extra *extra = destination->extra;
char file_name[ MAX_PATH ];
GList *item;
struct file_writer *writer;
TRACE_ENTER( "msg=%p, destination=%s\n", msg, destination->name );
make_file_name( destination->u.file.name_pattern, msg, file_name );
EnterCriticalSection( &extra->cs_file_writers );
/* find existing writer
FIXME: could we do this better?
*/
for( writer = NULL, item = extra->file_writers; item; item = item->next )
{
struct file_writer *w = item->data;
if( strcmp( w->file_name, file_name ) == 0 )
{
writer = w;
break;
}
}
if( !writer )
{
/* create new writer */
writer = create_file_writer( file_name, destination );
if( !writer )
goto done;
}
/* put message into queue */
reference_message( msg );
EnterCriticalSection( &writer->fifo_cs );
if( fifo_push( writer->message_queue, msg ) )
SetEvent( writer->fifo_event );
LeaveCriticalSection( &writer->fifo_cs );
done:
LeaveCriticalSection( &extra->cs_file_writers );
TRACE_LEAVE( "done\n" );
}
/******************************************************************************
* finalize_file_dest
*
* stop all writers
*/
static void finalize_file_dest( struct destination* destination )
{
struct dest_extra *extra = destination->extra;
GList *wr_item;
TRACE_ENTER( "destination=%s\n", destination->name );
/* setting shutdown event */
EnterCriticalSection( &extra->cs_file_writers );
for( wr_item = extra->file_writers; wr_item; wr_item = wr_item->next )
{
struct file_writer *writer = wr_item->data;
SetEvent( writer->shutdown_event );
}
LeaveCriticalSection( &extra->cs_file_writers );
TRACE_2( "waiting for shutdown\n" );
while( extra->file_writers )
{
Sleep( 60 );
}
TRACE_LEAVE( "done\n" );
}
/******************************************************************************
* init_destination_file
*
* initialize file destination
*/
gboolean init_destination_file( struct destination* destination )
{
struct dest_extra *extra = g_malloc( sizeof(struct dest_extra) );
destination->extra = extra;
destination->put = put_message_to_file_dest;
destination->fini = finalize_file_dest;
extra->file_writers = NULL;
InitializeCriticalSection( &extra->cs_file_writers );
return TRUE;
}