view mde/setup/Init.d @ 63:66d555da083e

Moved many modules/packages to better reflect usage.
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 27 Jun 2008 18:35:33 +0100
parents mde/scheduler/Init.d@f9f5e04f20b2
children cc3763817b8a
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 as published by the Free Software Foundation, either
version 2 of the License, or (at your option) any later version.

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, see <http://www.gnu.org/licenses/>. */

/**************************************************************************************************
 * 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.setup.Init;

import mde.setup.init2;     // This module is responsible for setting up some init functions.
import mde.setup.initFunctions;
import mde.setup.exception;

import mde.lookup.Options;
import paths = mde.setup.paths;
import mde.exception;

// tango imports
import tango.core.Thread;
import tango.core.Exception;
import tango.stdc.stringz : fromStringz;
import tango.io.Console;	// for printing command-line usage
import TimeStamp = tango.text.convert.TimeStamp, tango.time.WallClock;	// output date in log file
import tango.util.Arguments;
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;
}

// Derelict imports
import derelict.opengl.gl;
import derelict.sdl.sdl;
//import derelict.sdl.image;	Was to be used... for now isn't
import derelict.freetype.ft;
import derelict.util.exception;

/**
 * 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()
{
    Logger root = Log.getRootLogger();
    // 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);
    // Temporarily log to the console (until we've found paths and loaded options):
    root.addAppender(new ConsoleAppender);
}
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 bool failure = false;       // set true on failure during init, so that clean
    private static Logger logger;
    static this() {
        logger = Log.getLogger ("mde.setup.Init");
    }
    
    /** this() − initialisation
    *
    * Runs general initialisation code, in a threaded manner where this isn't difficult.
    * If any init fails, cleanup is still handled by ~this().
    *
    * Init order: 1. Pre-init (loads components needed by most init functions). 2. Dynamic library
    * loading (load any dynamic libraries first, so that if loading fails nothing else need be
    * done). 3. Init functions (threaded functions handling the rest of initialisation).
    */
    /* 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(char[][] cmdArgs)
    {
        debug logger.trace ("Init: starting");
        
        //BEGIN Pre-init (stage init0)
        //FIXME: warn on invalid arguments, including base-path on non-Windows
        // But Arguments doesn't support this (in tango 0.99.6 and in r3563).
        Arguments args;
        try {
            args = new Arguments();
            args.define("base-path").parameters(1);
            args.define("data-path").parameters(1,-1);
            args.define("conf-path").parameters(1,-1);
            args.define("paths");
            args.define("q").aliases(["quick-exit"]);
            args.define("help").aliases(["h"]);
            args.parse(cmdArgs);
            if (args.contains("help"))	// lazy way to print help
                throw new InitException ("Help requested");	// and stop
        } catch (Exception e) {
            printUsage(cmdArgs[0]);
            throw new InitException ("Parsing arguments failed: "~e.msg);
        }
        
	// Find/create paths:
	try {
            if (args.contains("data-path"))
                paths.extraDataPath = args["data-path"];
            if (args.contains("conf-path"))
                paths.extraConfPath = args["conf-path"];
            if (args.contains("base-path"))
                paths.resolvePaths (args["base-path"]);
            else
                paths.resolvePaths();
	} catch (Exception e) {
            throw new InitException ("Resolving paths failed: " ~ e.msg);
	}
        if (args.contains("paths")) {
            paths.mdeDirectory.printPaths;
            throw new InitException ("Paths requested");	// lazy way to stop
        }
        debug logger.trace ("Init: resolved paths successfully");
    
        /* Load options now. Don't load in a thread since:
        *   Loading should be fast & most work is probably disk access
        *   It enables logging to be controlled by options
        *   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: " ~ e.msg);
        }
        debug logger.trace ("Init: loaded options successfully");
        
	// Set up the logger:
        Logger root;
	try {
            enum LOG {
                LEVEL	= 0xF,		// mask for log level
                CONSOLE	= 0x1000,	// log to console?
                ROLLFILE= 0x2000	// use Rolling/Switching File Appender?
            }
            
	    // Where logging is done to is determined at compile-time, currently just via static ifs.
            root = Log.getRootLogger();
	    root.clearAppenders;	// we may no longer want to log to the console
	    
            // Now re-set the logging level, using the value from the config file:
            root.setLevel (cast(Log.Level) (miscOpts.logOptions & LOG.LEVEL), true);
            
            // Log to a file (first appender so root seperator messages don't show on console):
            if (miscOpts.logOptions & LOG.ROLLFILE) {
                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));
                    root.info (""); // some kind of separation between runs
                    root.info ("");
                }
            } else if (!(miscOpts.logOptions & LOG.CONSOLE)) {
                // make sure at least one logger is enabled
                Options.setInt ("misc", "logOptions", miscOpts.logOptions | LOG.CONSOLE);
            }
            if (miscOpts.logOptions & LOG.CONSOLE) {	// Log to the console
                root.addAppender(new ConsoleAppender);
            }
            logger.info ("Starting mde [no version] on " ~ TimeStamp.toString(WallClock.now));
        } catch (Exception e) {
            // Presumably it was only adding a file appender which failed; set up a new console
            // logger and if that fails let the exception kill the program.
            root.clearAppenders;
            root.addAppender (new ConsoleAppender);
            logger.warn ("Exception while setting up the logger; logging to the console instead.");
        }
        
        // a debugging option:
        imde.run = !args.contains("q") && !miscOpts.exitImmediately;
        debug logger.trace ("Init: applied pre-init options");
        
        //BEGIN Load dynamic libraries
        /* Can be done by init functions but much neater to do here.
        * Also means that init functions aren't run if a library fails to load. */
        try {
            DerelictSDL.load();
            // SDLImage was going to be used... for now it isn't because of gl texturing problems
            //DerelictSDLImage.load();
            DerelictGL.load();
            DerelictFT.load();
        } catch (DerelictException de) {
            logger.fatal ("Loading dynamic library failed:");
            logger.fatal (de.msg);
            
            throw new InitException ("Loading dynamic libraries failed (see above).");
        }
        debug logger.trace ("Init: dynamic libraries loaded");
        //END Load dynamic libraries
        //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. */
        
        try {
            if (runStageThreaded (init)) runStageForward (init);
        }
        catch (InitStageException) {    // This init stage failed.
            // FIXME: check DTOR still runs
            throw new InitException ("An init function failed (see above message(s))");
        }
        //END Init
        
        debug logger.trace ("Init: done");
    }
    
    /** DTOR - runs cleanup functions. */
    ~this()
    {
        debug logger.trace ("Cleanup: starting");
        
        Options.save(); // save options... do so here for now
        
        // General cleanup:
        try {
            if (runStageThreaded (cleanup)) runStageReverse (cleanup);
        }
        catch (InitStageException) {
            // Nothing else to do but report:
            logger.error ("One or more cleanup functions failed!");
        }
        
        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 LOG_IF_MSG = "Init function ";
        const LOG_CF_MSG = "Cleanup function ";
        const LOG_F_START = " - running";
        const LOG_F_END = " - completed";
        const LOG_F_BAD = " - failed";
        const LOG_F_FAIL = " - failed: ";
        /* 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 {
                    debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START);
                    func.func();
                    debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
                } catch (Exception e) {
                    logger.fatal (LOG_IF_MSG ~ func.name ~ LOG_F_FAIL ~
                            ((e.msg is null || e.msg == "") ? "(no failure message)" : 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 {
                    debug logger.trace (LOG_CF_MSG ~ func.name ~ LOG_F_START);
                    func.func();
                    debug logger.trace (LOG_CF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
                } catch (Exception e) {
                    logger.fatal (LOG_CF_MSG ~ func.name ~ LOG_F_FAIL ~
                            ((e.msg is null || e.msg == "") ? "(no failure message)" : 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) {   // Start all threads
                    debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START);
                    tg.create(func.func);
                    debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
                }
            } 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, assuming no threads were created.");
            
                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...
        
        void printUsage (char[] progName) {
            Cout ("mde [no version]").newline;
            Cout ("Usage:").newline;
            Cout (progName ~ ` [options]`).newline;
            version(Windows)
                    Cout (
`  --base-path path	Use path as the base (install) path (Windows only). It
			should contain the "data" directory.`).newline;
            Cout (
`  --data-path path(s)	Add path(s) as a potential location for data files.
			First path argument becomes the preffered location to
			load data files from.
  --conf-path path(s)	Add path(s) as a potential location for config files.
			Configuration in the first path given take highest
			priority.
  --paths		Print all paths found and exit.
  --quick-exit, -q	Exit immediately, without entering main loop.
  --help, -h		Print this message.`).newline;
        }
    }
    
    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, "UT cleanup 1");
            cleanupUT.addFunc (&cleanupFunc2, "UT cleanup 2");
        }
        
        initUT.addFunc (&initFunc, "UT init");
        
        runStageForward (initUT);
        assert (initialised);
        
        runStageReverse (cleanupUT);
        assert (!initialised);

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