Mixer: Implement cut/copy/paste for modules.
[nondaw.git] / nonlib / Loggable.C
blob61e0062770ab9dfeefe33c53f7b617592bd93d4a
2 /*******************************************************************************/
3 /* Copyright (C) 2008 Jonathan Moore Liles                                     */
4 /*                                                                             */
5 /* This program is free software; you can redistribute it and/or modify it     */
6 /* under the terms of the GNU General Public License as published by the       */
7 /* Free Software Foundation; either version 2 of the License, or (at your      */
8 /* option) any later version.                                                  */
9 /*                                                                             */
10 /* This program is distributed in the hope that it will be useful, but WITHOUT */
11 /* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       */
12 /* FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for   */
13 /* more details.                                                               */
14 /*                                                                             */
15 /* You should have received a copy of the GNU General Public License along     */
16 /* with This program; see the file COPYING.  If not,write to the Free Software */
17 /* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */
18 /*******************************************************************************/
20 /* This class handles all journaling. All journaled objects must
21    inherit from Loggable as well as define a few special methods (via
22    macros), get and set methods, and have contructors and destructors
23    that call log_create() and log_destroy() in the appropriate
24    order. Any action that might affect multiple loggable objects
25    *must* be braced by calls to Loggable::block_start() and
26    Loggable::block_end() in order for Undo to work properly. */
28 #include "Loggable.H"
30 #include <stdlib.h>
31 #include <stdio.h>
32 #include <string.h>
34 #include "util/file.h"
36 // #include "const.h"
37 #include "util/debug.h"
39 #include <algorithm>
40 using std::min;
41 using std::max;
45 FILE *Loggable::_fp;
46 unsigned int Loggable::_log_id = 0;
47 int Loggable::_level = 0;
49 off_t Loggable::_undo_offset = 0;
51 std::map <unsigned int, Loggable::log_pair > Loggable::_loggables;
53 std::map <std::string, create_func*> Loggable::_class_map;
54 std::queue <char *> Loggable::_transaction;
56 progress_func *Loggable::_progress_callback = NULL;
57 void *Loggable::_progress_callback_arg = NULL;
59 snapshot_func *Loggable::_snapshot_callback = NULL;
60 void *Loggable::_snapshot_callback_arg = NULL;
64 Loggable::~Loggable ( )
66     _loggables[ _id ].loggable = NULL;
71 void
72 Loggable::block_start ( void )
74     ++Loggable::_level;
77 void
78 Loggable::block_end ( void )
80     --Loggable::_level;
82     ASSERT( Loggable::_level >= 0, "Programming error" );
84     if ( Loggable::_level == 0 )
85         flush();
88 Loggable *
89 Loggable::find ( unsigned int id )
91     return _loggables[ id ].loggable;
94 /** Open the journal /filename/ and replay it, bringing the end state back into RAM */
95 bool
96 Loggable::open ( const char *filename )
98     FILE *fp;
100     Loggable::_fp = NULL;
102     if ( ! ( fp = fopen( filename, "a+" ) ) )
103     {
104         WARNING( "Could not open log file for writing!" );
105         return false;
106     }
108     load_unjournaled_state();
110     if ( newer( "snapshot", filename ) )
111     {
112         MESSAGE( "Loading snapshot" );
114         FILE *fp = fopen( "snapshot", "r" );
116         replay( fp );
118         fclose( fp );
119     }
120     else
121     {
122         MESSAGE( "Replaying journal" );
124         replay( fp );
125     }
127     fseek( fp, 0, SEEK_END );
128     _undo_offset = ftell( fp );
130     Loggable::_fp = fp;
132     return true;
135 bool
136 Loggable::load_unjournaled_state ( void )
138     FILE *fp;
140     fp = fopen( "unjournaled", "r" );
142     if ( ! fp )
143     {
144         DWARNING( "Could not open unjournaled state file for reading" );
145         return false;
146     }
148     unsigned int id;
149     char buf[BUFSIZ];
151     while ( fscanf( fp, "%X set %[^\n]\n", &id, buf ) == 2 )
152         _loggables[ id ].unjournaled_state = new Log_Entry( buf );
154     fclose( fp );
156     return true;
159 #include <sys/stat.h>
160 #include <unistd.h>
162 /** replay journal or snapshot */
163 bool
164 Loggable::replay ( const char *file )
166     if ( FILE *fp = fopen( file, "r" ) )
167     {
168         bool r = replay( fp );
170         fclose( fp );
172         return r;
173     }
174     else
175         return false;
178 /** replay journal or snapshot */
179 bool
180 Loggable::replay ( FILE *fp )
182     /* FIXME: bogus */
183     char buf[BUFSIZ];
185     struct stat st;
186     fstat( fileno( fp ), &st );
188     off_t total = st.st_size;
189     off_t current = 0;
191     if ( _progress_callback )
192         _progress_callback( 0, _progress_callback_arg );
194     while ( fscanf( fp, "%[^\n]\n", buf ) == 1 )
195     {
196         if ( ! ( ! strcmp( buf, "{" ) || ! strcmp( buf, "}" ) ) )
197         {
198             if ( *buf == '\t' )
199                 do_this( buf + 1, false );
200             else
201                 do_this( buf, false );
202         }
204         current = ftell( fp );
206         if ( _progress_callback )
207             _progress_callback( current * 100 / total, _progress_callback_arg );
208     }
210     if ( _progress_callback )
211         _progress_callback( 0, _progress_callback_arg );
213     return true;
216 /** close journal and delete all loggable objects, returing the systemt to a blank slate */
217 bool
218 Loggable::close ( void )
220     DMESSAGE( "closing journal and destroying all journaled objects" );
222     if ( _fp )
223     {
224         fclose( _fp );
225         _fp = NULL;
226     }
228     if ( ! snapshot( "snapshot" ) )
229         WARNING( "Failed to create snapshot" );
231     for ( std::map <unsigned int, Loggable::log_pair >::iterator i = _loggables.begin();
232           i != _loggables.end(); ++i )
233         if ( i->second.loggable )
234             delete i->second.loggable;
236     save_unjournaled_state();
238     for ( std::map <unsigned int, Loggable::log_pair >::iterator i = _loggables.begin();
239           i != _loggables.end(); ++i )
240         if ( i->second.unjournaled_state )
241             delete i->second.unjournaled_state;
243     _loggables.clear();
245     return true;
249 /** save out unjournaled state for all loggables */
250 bool
251 Loggable::save_unjournaled_state ( void )
253     FILE *fp;
255     fp = fopen( "unjournaled", "w" );
257     if ( ! fp )
258     {
259         DWARNING( "Could not open unjournaled state file for writing!" );
260         return false;
261     }
263     for ( std::map <unsigned int, Loggable::log_pair >::iterator i = _loggables.begin();
264           i != _loggables.end(); ++i )
265     {
266         if ( i->second.unjournaled_state )
267         {
268             char *s = i->second.unjournaled_state->print();
270             fprintf( fp, "0x%X set %s\n", i->first, s );
272             free( s );
273         }
274     }
276     fclose( fp );
278     return true;
281 /** must be called after construction in create() methods */
282 void
283 Loggable::update_id ( unsigned int id )
285     /* make sure we're the last one */
286     ASSERT( _id == _log_id, "%u != %u", _id, _log_id );
287     assert( _loggables[ _id ].loggable == this );
289     _loggables[ _id ].loggable = NULL;
291     _log_id = max( _log_id, id );
293     /* return this id number to the system */
294 //            --_log_id;
296     _id = id;
298     if ( _loggables[ _id ].loggable )
299         FATAL( "Attempt to create object with an ID (0x%X) that already exists. The existing object is of type \"%s\", the new one is \"%s\". Corrupt journal?", _id, _loggables[ _id ].loggable->class_name(), class_name() );
301     _loggables[ _id ].loggable = this;
304 /** return a pointer to a static copy of /s/ with all special characters escaped */
305 const char *
306 Loggable::escape ( const char *s )
308     static char r[512];
310     size_t i = 0;
311     for ( ; *s && i < sizeof( r ); ++i, ++s )
312     {
313         if ( '\n' == *s )
314         {
315             r[ i++ ] = '\\';
316             r[ i ] = 'n';
317         }
318         else if ( '"' == *s )
319         {
320             r[ i++ ] = '\\';
321             r[ i ] = '"';
322         }
323         else
324             r[ i ] = *s;
325     }
327     r[ i ] = '\0';
329     return r;
332 /** 'do' a message like "Audio_Region 0xF1 set :r 123" */
333 bool
334 Loggable::do_this ( const char *s, bool reverse )
336     unsigned int id = 0;
338     char classname[40];
339     char command[40];
340     char *arguments = NULL;
342     int found = sscanf( s, "%s %X %s ", classname, &id, command );
344     ASSERT( 3 == found,  "Invalid journal entry format \"%s\"", s );
346     const char *create, *destroy;
348     if ( reverse )
349     {
350 //        sscanf( s, "%s %*X %s %*[^\n<]<< %a[^\n]", classname, command, &arguments );
351         sscanf( s, "%s %*X %s%*[^\n<]<< %a[^\n]", classname, command, &arguments );
352         create = "destroy";
353         destroy = "create";
355         DMESSAGE( "undoing \"%s\"", s );
356     }
357     else
358     {
359         sscanf( s, "%s %*X %s %a[^\n<]", classname, command, &arguments );
360         create = "create";
361         destroy = "destroy";
362     }
364     if ( ! strcmp( command, destroy ) )
365     {
366         Loggable *l = find( id );
368         /* deleting eg. a track, which contains a list of other
369            widgets, causes destroy messages to be emitted for all those
370            widgets, but when replaying the journal the destroy message
371            causes the children to be deleted also... This is a temporary
372            hack. Would it be better to queue up objects for deletion
373            (when?) */
374         if ( l )
375             delete l;
376     }
377     else if ( ! strcmp( command, "set" ) )
378     {
379 //        printf( "got set command (%s).\n", arguments );
381         Loggable *l = find( id );
383         ASSERT( l, "Unable to find object 0x%X referenced by command \"%s\"", id, s );
385         Log_Entry e( arguments );
387         l->log_start();
388         l->set( e );
389         l->log_end();
390     }
391     else if ( ! strcmp( command, create ) )
392     {
393         Log_Entry e( arguments );
395         ASSERT( _class_map[ std::string( classname ) ], "Journal contains an object of class \"%s\", but I don't know how to create such objects.", classname );
397         {
398             /* create */
399             Loggable *l = _class_map[ std::string( classname ) ]( e, id );
400             l->log_create();
402             /* we're now creating a loggable. Apply any unjournaled
403              * state it may have had in the past under this log ID */
405             Log_Entry *e = _loggables[ id ].unjournaled_state;
407             if ( e )
408                 l->set( *e );
409         }
411     }
413     if ( arguments )
414         free( arguments );
416     return true;
419 /** Reverse the last journal transaction */
420 void
421 Loggable::undo ( void )
423     const int bufsiz = 1024;
424     char buf[bufsiz];
426     block_start();
428     long here = ftell( _fp );
430     fseek( _fp, _undo_offset, SEEK_SET );
432     backwards_fgets( buf, bufsiz, _fp );
434     if ( ! strcmp( buf, "}\n" ) )
435     {
436         DMESSAGE( "undoing block" );
437         for ( ;; )
438         {
439             backwards_fgets( buf, bufsiz, _fp );
441             char *s = buf;
442             if ( *s != '\t' )
443                 break;
444             else
445                 ++s;
447             do_this( s, true );
448         }
449     }
450     else
451         do_this( buf, true );
453     off_t uo = ftell( _fp );
455     ASSERT( _undo_offset <= here, "WTF?" );
457     block_end();
459     _undo_offset = uo;
462 /** write a snapshot of the current state of all loggable objects to
463  * file handle /fp/ */
464 bool
465 Loggable::snapshot ( FILE *fp )
467     FILE *ofp = _fp;
469     if ( ! Loggable::_snapshot_callback )
470     {
471         DWARNING( "No snapshot callback defined" );
472         return false;
473     }
475     if ( ! ( _fp = fp ) )
476     {
477         _fp = ofp;
478         return false;
479     }
481     block_start();
483     Loggable::_snapshot_callback( _snapshot_callback_arg );
485     block_end();
487     _fp = ofp;
489     return true;
492 /** write a snapshot of the current state of all loggable objects to
493  * file /name/ */
494 bool
495 Loggable::snapshot ( const char *name )
497     FILE *fp;
499     if ( ! ( fp = fopen( name, "w" ) ))
500         return false;
502     bool r = snapshot( fp );
504     fclose( fp );
506     return r;
509 /** Replace the journal with a snapshot of the current state */
510 void
511 Loggable::compact ( void )
513     fseek( _fp, 0, SEEK_SET );
514     ftruncate( fileno( _fp ), 0 );
516     if ( ! snapshot( _fp ) )
517         FATAL( "Could not write snapshot!" );
519     fseek( _fp, 0, SEEK_END );
522 #include <stdarg.h>
524 /** Writes (part of) a line to the journal. Each separate line will be
525  * stored separately in _transaction until transaction is closed.
526  */
527 void
528 Loggable::log ( const char *fmt, ... )
530     static char * buf = NULL;
531     static size_t i = 0;
532     static size_t buf_size = 0;
534     if ( ! _fp )
535         return;
537     if ( NULL == buf )
538     {
539         buf_size = 1024;
540         buf = (char*)malloc( buf_size );
541     }
543     va_list args;
545     if ( fmt )
546     {
547         va_start( args, fmt );
549         for ( ;; )
550         {
551             size_t l = vsnprintf( buf + i, buf_size - i, fmt, args );
553             if ( l >= buf_size - i )
554             {
555                 buf = (char*)realloc( buf, buf_size += (l + 1) + buf_size );
556             }
557             else
558             {
559                 i += l;
560                 break;
561             }
562         }
564         va_end( args );
565     }
567     if ( '\n' == buf[i-1] )
568     {
569         _transaction.push( strdup( buf ) );
570         i = 0;
571     }
574 /** End the current transaction and commit it to the journal */
575 void
576 Loggable::flush ( void )
578     if ( ! _fp )
579     {
580 //        printf( "error: no log file open!\n" );
582         while ( ! _transaction.empty() )
583         {
584             free( _transaction.front() );
585             _transaction.pop();
586         }
588         return;
589     }
591     int n = _transaction.size();
593     if ( n > 1 )
594         fprintf( _fp, "{\n" );
596     while ( ! _transaction.empty() )
597     {
598         char *s = _transaction.front();
600         _transaction.pop();
602         if ( n > 1 )
603             fprintf( _fp, "\t" );
605         fprintf( _fp, "%s", s );
607         free( s );
608     }
610     if ( n > 1 )
611         fprintf( _fp, "}\n" );
613     if ( n )
614         /* something done, reset undo index */
615         _undo_offset = ftell( _fp );
617     fflush( _fp );
620 /** Print bidirectional journal entry */
621 void
622 Loggable::log_print( const Log_Entry *o, const Log_Entry *n ) const
624     if ( ! _fp )
625         return;
627     if ( n )
628         for ( int i = 0; i < n->size(); ++i )
629         {
630             const char *s, *v;
632             n->get( i, &s, &v );
634             log( "%s %s%s", s, v, n->size() == i + 1 ? "" : " " );
635         }
637     if ( o && o->size() )
638     {
639         if ( n ) log( " << " );
641         for ( int i = 0; i < o->size(); ++i )
642         {
643             const char *s, *v;
645             o->get( i, &s, &v );
647             log( "%s %s%s", s, v, o->size() == i + 1 ? "" : " " );
648         }
649     }
651     log( "\n" );
654 /** Remember current object state for later comparison. *Must* be
655  * called before any user action that might change one of the object's
656  * journaled properties.  */
657 void
658 Loggable::log_start ( void )
660     if ( ! _old_state )
661     {
662         _old_state = new Log_Entry;
664         get( *_old_state );
665     }
666     ++_nest;
669 /** Log any change to the object's state since log_start(). */
670 void
671 Loggable::log_end ( void )
673     ASSERT( _old_state, "Programming error: log_end() called before log_start()" );
675     if ( --_nest > 0 )
676         return;
678     Log_Entry *new_state;
680     new_state = new Log_Entry;
682     get( *new_state );
684     if ( Log_Entry::diff( _old_state, new_state ) )
685     {
686         log( "%s 0x%X set ", class_name(), _id );
688         log_print( _old_state, new_state );
689     }
691     delete new_state;
692     delete _old_state;
694     _old_state = NULL;
696     if ( Loggable::_level == 0 )
697         Loggable::flush();
700 /** Log object creation. *Must* be called at the end of all public
701  * constructors for leaf classes  */
703 void
704 Loggable::log_create ( void ) const
706     if ( ! _fp )
707         /* replaying, don't bother */
708         return;
710     log( "%s 0x%X create ", class_name(), _id );
712     Log_Entry e;
714     get( e );
716     if ( e.size() )
717         log_print( NULL, &e );
718     else
719         log( "\n" );
721     if ( Loggable::_level == 0 )
722         Loggable::flush();
725 /** record this loggable's unjournaled state in memory */
726 void
727 Loggable::record_unjournaled ( void ) const
729     Log_Entry *e = new Log_Entry();
731     get_unjournaled( *e );
733     Log_Entry *le = _loggables[ _id ].unjournaled_state;
735     if ( le )
736         delete le;
738     if ( e->size() )
739         _loggables[ _id ].unjournaled_state = e;
740     else
741     {
742         /* don't waste space on loggables with no unjournaled properties */
743         _loggables[ _id ].unjournaled_state = NULL;
744         delete e;
745     }
748 /** Log object destruction. *Must* be called at the beginning of the
749  * destructors of leaf classes */
750 void
751 Loggable::log_destroy ( void ) const
753     /* the unjournaled state may have changed: make a note of it. */
754     record_unjournaled();
756     if ( ! _fp )
757         /* tearing down... don't bother */
758         return;
760     log( "%s 0x%X destroy << ", class_name(), _id );
762     Log_Entry e;
764     get( e );
766     log_print( NULL, &e );
768     if ( Loggable::_level == 0 )
769         Loggable::flush();