view mde/scheduler/Init.d @ 24:32eff0e01c05

Only locally-changed options are stored in user-config now. Log levels revised. Options sub-classes are handled more generically and can be added without changing the Options class. Options changed at run-time are tracked, and on exit merged with user options and saved. Revised log levels as set out in policies.txt and as used in code. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 27 Mar 2008 16:15:21 +0000
parents a60cbb7359dd
children 2c28ee04a4ed
line wrap: on
line source

/* LICENSE BLOCK
Part of mde: a Modular D game-oriented Engine
Copyright © 2007-2008 Diggory Hardy

This program is free software; you can redistribute it and/or modify it under the terms of
the GNU General Public License, version 2, as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */

/**************************************************************************************************
 * Initialisation setup and exit cleanup module.
 *
 * This module provides an infrastructure for handling much of the initialisation and
 * deinitialisation of the program. It does not, however, provide much of the (de)initialisation
 * code; with the exception of that for the logger.
 *************************************************************************************************/
module mde.scheduler.Init;

import mde.scheduler.InitStage;
import mde.scheduler.exception;

import mde.options;
import paths = mde.resource.paths;
import mde.exception;

// tango imports
import tango.core.Thread;
import tango.core.Exception;
import tango.stdc.stringz : fromStringz;
import tango.util.log.Log : Log, Logger;
import tango.util.log.ConsoleAppender : ConsoleAppender;

version = SwitchAppender;
version (SwitchAppender) {  // My own variation, currently just a test
    import tango.util.log.SwitchingFileAppender : SwitchingFileAppender;
} else {
    import tango.util.log.RollingFileAppender : RollingFileAppender;
}

/**
 * Static CTOR
 *
 * This should handle a minimal amount of functionality where useful. For instance, configuring the
 * logger here and not in Init allows unittests to use the logger.
 */
static this()
{
    // Find/create paths:
    try {
        paths.resolvePaths();
    } catch (Exception e) {
        throw new InitException ("Resolving paths failed: " ~ e.msg);
    }
    
    // Set up the logger:
    {
        // Where logging is done to is determined at compile-time, currently just via static ifs.
        Logger root = Log.getRootLogger();
                
        static if (true ) { // Log to the console
            root.addAppender(new ConsoleAppender);
        }
        static if (true ) { // Log to files
            version (SwitchAppender) {
                root.addAppender (new SwitchingFileAppender (paths.logDir~"/log-.txt", 5));
            } else {
                // Use 2 log files with a maximum size of 1 MB:
                root.addAppender (new RollingFileAppender (paths.logDir~"/log-.txt", 2, 1024*1024));
            }
        }
        
        // Set the level here, but set it again once options have been loaded:
        debug root.setLevel(root.Level.Trace);
        else root.setLevel(root.Level.Info);
    }
}
static ~this()
{
}

/**
 * Init class
 *
 * A scope class created at beginning of the program and destroyed at the end; thus the CTOR
 * handles program initialisation and the DTOR handles program cleanup.
 */
scope class Init
{
    private static Logger logger;
    static this() {
        logger = Log.getLogger ("mde.scheduler.Init.Init");
    }
    
    /** CTOR − initialisation
    *
    * Runs general initialisation code, in a threaded manner where this isn't difficult.
    *
    * If any init fails, it must run necessary cleanup first since the DTOR cannot(?) be run. */
    /* In a single-threaded function this could be done with:
    * scope(failure) cleanup;
    * This won't work with a threaded init function since any threads completing succesfully will
    * not clean-up, and a fixed list of clean-up functions cannot be used since clean-up functions
    * must not run where the initialisation functions have failed.
    * Hence a list of clean-up functions is built similarly to scope(failure) --- see addCleanupFct.
    */
    this()
    {
        debug logger.trace ("Init: starting");
        
        //BEGIN Pre-init (stage init0)
        /* Load options now. Don't load in a thread since:
        *   Loading should be fast
        *   Most work is probably disk access
        *   It's a really good idea to let the options apply to all other loading. */
        try {
            Options.load();
        } catch (optionsLoadException e) {
            throw new InitException ("Loading options failed; message: " ~ e.msg);
        }
        
        // Now re-set the logging level:
        Log.getRootLogger.setLevel (cast(Log.Level) miscOpts.logLevel, true);  // set the stored log level
        //END Pre-init
        
        
        //BEGIN Init (stages init2, init4)
        /* Call init functions.
        *
        * Current method is to try using threads, and on failure assume no threads were actually
        * created and run functions in a non-threaded manner. */
        
        // init2
        cleanupStages ~= &cleanup2;     // add appropriate cleanup stage
        try {
            debug logger.trace ("Init: init2");
            if (runStageThreaded (init2)) runStageForward (init2);
        }
        catch (InitStageException) {    // This init stage failed.
            debug logger.trace ("Init: init2 failed");
            runCleanupStages();
            throw new InitException ("Initialisation failed during stage init2");
        }
        
        // init4
        cleanupStages ~= &cleanup4;     // add appropriate cleanup stage
        try {
            debug logger.trace ("Init: init4");
            if (runStageThreaded (init4)) runStageForward (init4);
        }
        catch (InitStageException) {    // This init stage failed.
            debug logger.trace ("Init: init4 failed");
            runCleanupStages();
            throw new InitException ("Initialisation failed during stage init4");
        }
        //END Init
        
        debug logger.trace ("Init: done");
    }
    
    /** DTOR - runs cleanup functions. */
    ~this()
    {
        debug logger.trace ("Cleanup: starting");
        
        // cleanup1:
        Options.save(); // save options... do so here for now
        
        // cleanup2, 4:
        runCleanupStages();
        
        debug logger.trace ("Cleanup: done");
    }
    
    
    //BEGIN runStage...
    private static {
        /* The following three functions, runStage*, each run all functions in a stage in some order,
        * catching any exceptions thrown by the functions (although this isn't guaranteed for threads),
        * and throw an InitStageException on initFailure. */
    
        const UFE = "Unhandled exception from Init function:";
        /* Runs all functions consecutively, first-to-last.
        * If any function fails, halts immediately. */
        void runStageForward (InitStage s) {
            foreach (func; s.funcs) {
                if (initFailure) break;
                try {
                    func();
                } catch (Exception e) {
                    logger.fatal (UFE);
                    logger.fatal (e.msg);
                
                    setInitFailure();
                }
            }
            
            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
        }
        /* Runs all functions consecutively, last-to-first.
        * If any function fails, continue until all have been run. */
        void runStageReverse (InitStage s) {
            foreach_reverse (func; s.funcs) {
                try {
                    func();
                } catch (Exception e) {
                    logger.fatal (UFE);
                    logger.fatal (e.msg);
                
                    setInitFailure();
                }
            }
            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
        }
        /* Tries running functions in a threaded way. Returns false if successful, true if not but
        * functions should be run without threads. */
        bool runStageThreaded (InitStage s) {
            if (!miscOpts.useThreads) return true;  // Use unthreaded route instead
        
            ThreadGroup tg;
            try {                           // creating/starting threads could fail
                tg = new ThreadGroup;
                foreach (func; s.funcs) tg.create(func);  // Start all threads
            } catch (ThreadException e) {   // Problem with threading; try without threads
                logger.error ("Caught ThreadException while trying to create threads:");
                logger.error (e.msg);
                logger.info ("Will disable threads and continue.");
            
                Options.setBool("misc", "useThreads", false);   // Disable threads entirely
                return true;                // Try again without threads
            }
        
            /* Wait for all threads to complete.
            *
            * If something went wrong, we still need to do this before cleaning up.
            */
            foreach (t; tg) {
                try {
                    t.join (true);
                } catch (Exception e) {
                    // Relying on catching exceptions thrown by other threads is a bad idea.
                    // Hence all threads should catch their own exceptions and return safely.
                
                    logger.fatal ("Unhandled exception from Init function:");
                    logger.fatal (e.msg);
                
                    setInitFailure ();        // abort (but join other threads first)
                }
            }
            
            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
            return false;                   // Done successfully
        }
    }
    //END runStage...
    
    private {
        InitStage*[] cleanupStages;         // All cleanup stages needing to be run later.
        void runCleanupStages () {
            foreach_reverse (s; cleanupStages) {
                try {
                    runStageReverse (*s);
                }
                catch (InitStageException) {
                    // We're cleaning up anyway, just report and continue
                    logger.error ("Cleanup function failed! Continuing...");
                }
            }
            cleanupStages = [];             // All have been run, don't want them getting run again
        }
    }
    
    debug (mdeUnitTest) unittest {
        /* Fake init and cleanup. Use unittest-specific init and cleanup InitStages to avoid
        * messing other init/cleanup up. */
        static InitStage initUT, cleanupUT;
        
        static bool initialised = false;
        static void cleanupFunc1 () {
            initialised = false;
        }
        static void cleanupFunc2 () {
            assert (initialised == true);
        }
                
        static void initFunc () {
            initialised = true;
            cleanupUT.addFunc (&cleanupFunc1);
            cleanupUT.addFunc (&cleanupFunc2);
        }
        
        initUT.addFunc (&initFunc);
        
        runStageForward (initUT);
        assert (initialised);
        
        runStageReverse (cleanupUT);
        assert (!initialised);

        logger.info ("Unittest complete.");
    }
}