view mde/Init.d @ 18:56a42ec95024

Changes to Init and logging. Moved an Init function; hence events now depends on Init rather than vice-versa. Paths and logging set up by Init's static this(). Logging location set at compile time. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Tue, 18 Mar 2008 17:51:52 +0000
parents 5f90774ea1ef
children db0b48f02b69
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.Init;

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

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

/**
 * 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:
    version (mdeTest) {}    // test.mdeTest sets up its own root logger
    else {
        // 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
            // Use 2 log files with a maximum size of 1 MB:
            root.addAppender (new SwitchingFileAppender (paths.logDir~"/log-.txt", 5));
        }
        
        // 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()
{
}

// Global instance, since called init functions need to interact:
scope Init init;

/**
 * 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.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()
    {
        logger.info ("Init: starting");
        
        //BEGIN Pre-init (loading options)
        /* 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);
        }
        addCleanupFct (&Options.save);  // not strictly cleanup, but needs to be called somewhere
        
        // Now re-set the logging level:
        Log.getRootLogger.setLevel (cast(Log.Level) Options.misc.logLevel, true);  // set the stored log level
        //END Pre-init
        
        
        //BEGIN Init (actual function calling)
        /* 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.
        */
        ThreadGroup tg;                     // Not necessarily used
        
        if (Options.misc.useThreads) {           // Threaded init (thread creation)
            try {                           // creating/starting threads could fail
                tg = new ThreadGroup;
                foreach (func; initFuncs) tg.create(func);  // Start all threads
            } catch (ThreadException e) {   // Problem with threading; try without threads
                logger.warn ("Caught exception while trying to create threads:");
                logger.warn (e.msg);
                logger.warn ("Will continue in a non-threaded manner.");
            
                Options.misc.useThreads = false;
            }
        }
        
        if (!Options.misc.useThreads) {          // Non-threaded init
            
            foreach (func; initFuncs) {     // for each delegate
                // since we can stop at any point, we might as well if there's a problem:
                if (initFailure) break;
            
                func();
            }
        }
        
        if (Options.misc.useThreads) {           // Threaded init (thread joining)
            /* Wait for all threads to complete.
            *
            * If something went wrong, we still need to do this before cleaning up.
            *
            * For non-threaded usage, tg contains no threads so loop won't run.
            */
            foreach (t; tg) {
                try {
                    t.join (true);
                    /+ We might as well catch and report any exception, rather than just ThreadExceptions:
                    } catch (ThreadException e) {	// Any threading exception
                    +/
                } catch (Exception e) {		// Any other exception, i.e. caught from thread.
                    // 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 ("Exception caught during init:");
                    logger.fatal (e.msg);
                    logger.fatal ("Please report this; it should NEVER happen.");
                
                    setFailure ();		// abort (but join other threads first)
                }
            }
        }
        
        if (initFailure) {
            // All cleanup-on-failure must be done here.
            runCleanupFcts();
            
            logger.fatal ("Init: failed");
            // Throw an exception to signal failure and prevent DTOR from also running.
            throw new InitException ("Initialisation failed due to above exceptions.");
        } else logger.info ("Init: done");
        //END Init
    }
    
    /** DTOR - runs cleanup.
    *
    * Currently unthreaded; probably might as well stay that way. */
    ~this()
    {
        if (!initFailure) {
            logger.trace ("Init: cleaning up");
            runCleanupFcts();	// if threading, note not all functions can be called simultaeneously
            logger.trace ("Init: done");
        }
    }
    
    // Stop init and start cleanup.
    void setFailure () {
        synchronized initFailure = true;
    }
    
    static {
        /* Initialisation functions.
        *
        * These should each handle a separate area of initialisation so that these functions can
        * be run simultaneously in separate threads. */
        private void function() [] initFuncs;
        
        // Add another init function. Should be called from a static this().
        static void addFunc (void function() fct) {
            initFuncs ~= fct;
        }
        
        private void function ()[] cleanup;		// all functions to be run for cleanup
        
        // Adding cleanup functions must be synchronized; use:
        void addCleanupFct (void function () fct) {
            synchronized cleanup ~= fct;
        }
        
        // Clean-up fcts are run in reverse order to how they're added:
        private void runCleanupFcts () {
            foreach_reverse (fct; cleanup) fct();
        }
    }
    
    /* This is set true by this() if something goes wrong and we need to abort.
    * If it's true, no cleanup should be done by the DTOR (since the DTOR still runs after an
    * exception has been thrown). */
    private bool initFailure = false;
    
    debug (mdeUnitTest) unittest {
        /* Fake init and cleanup. This happens before the CTOR runs so the extra Init.runCleanupFcts()
        * call isn't going to mess things up. The extra function called by runCleanupFcts won't cause
        * any harm either. */
        static bool initialised = false;
        static void cleanup () {	initialised = false;	}
        
        static void init () {
            initialised = true;
            Init.addCleanupFct (&cleanup);
        }
        
        init();
        assert (initialised);
        Init.runCleanupFcts();
        assert (!initialised);

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