view mde/setup/Init.d @ 91:4d5d53e4f881

Shared alignment for dynamic content lists - finally implemented! Lots of smaller changes too. Some debugging improvements. When multiple .mtt files are read for merging, files with invalid headers are ignored and no error is thrown so long as at least one file os valid.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 16 Oct 2008 17:43:48 +0100
parents 97e6dce08037
children 9520cc0448e5
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 and cleanup (shutdown) module.
 *
 * Program startup follows this sequence: static this() functions, pre-init, init.
 * Shutdown consists of: cleanup, post-cleanup, static ~this() functions.
 * 
 * static this and ~this functions should not use any external resources, such as dynamically
 * loaded libraries or files, and should not interact with other modules. They should be almost
 * guaranteed not to fail. Preferably, they shouldn't involve large amounts of memory or
 * processing, but this is not a requirement.
 * 
 * Pre-init: init code written in this module. Generally only prerequisets of most other stages
 * go here.
 * 
 * Init: This is where init code from external modules gets hooked in. Each stage consists of an
 * initialization function, a corresponding cleanup function, a state, and any dependencies (other
 * init functions which must be run before this one). Init functions are run according to these
 * dependencies, potentially threaded simultaeneously with other init functions.
 * 
 * Cleanup: Cleanup happens similarly to init for all stages requiring shutdown (according to their
 * state). The init dependencies are applied in reverse order (so if X depended on Y, Y's cleanup
 * will not run until X's cleanup has completed), and again the functions may be threaded.
 * 
 * Post-cleanup: like pre-init, this is code written in Init.
 *************************************************************************************************/
module mde.setup.Init;

import mde.setup.InitStage;     // Controls external delegates run by init
import mde.setup.exception;

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

// tango imports
import tango.core.Thread;
import tango.core.sync.Condition;
import tango.core.Exception;
import tango.util.container.LinkedList;

//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;
import tango.util.log.AppendConsole;
import tango.util.log.AppendFiles;

// 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;


/**************************************************************************************************
 * 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
{
    static this() {
        // Set up the logger temporarily (until pre-init):
        Logger root = Log.root;
        debug root.level(Logger.Trace);
        else  root.level(Logger.Info);
        root.add(new AppendConsole);
        
        logger = Log.getLogger ("mde.setup.Init");
    }
    
    /** this() − pre-init and init */
    this(char[][] cmdArgs)
    {
        /******************************************************************************************
         * Pre-init - init code written in this module.
         *****************************************************************************************/
        debug logger.trace ("Init: starting pre-init");
        //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.root;
	    root.clear;			// we may no longer want to log to the console
	    
            // Now re-set the logging level, using the value from the config file:
            root.level (cast(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) {
                // Use 2 log files with a maximum size of 1 MB:
                root.add (new AppendFiles (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
                miscOpts.set!(int) ("logOptions", miscOpts.logOptions | LOG.CONSOLE);
            }
            if (miscOpts.logOptions & LOG.CONSOLE) {	// Log to the console
                root.add(new AppendConsole);
            }
            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.clear;
            root.add (new AppendConsole);
            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
        
        
        /******************************************************************************************
         * Init − where init code from external modules gets hooked in.
         *****************************************************************************************/
        debug logger.trace ("Init: done pre-init, starting init stages");
        
        // Calculate reverse dependencies of stages:
        foreach (key,stage_p; stages)
            foreach (name; stage_p.depends)
                stages[name].rdepends ~= key;
        
        runStages!(true);       // startup delegates
        
        debug logger.trace ("Init: done");
    }
    
    /** DTOR - runs cleanup functions. */
    ~this()
    {
        debug logger.trace ("Cleanup: starting");
        
        runStages!(false);      // cleanup delegates
        
        Options.save(); // save options... do so here for now
        
        debug logger.trace ("Cleanup: done");
    }
    
    // run init stages or cleanup if startup is false
    private static void runStages(bool startup) () {
        auto toRun = new LinkedList!(InitStage*);
        foreach (v; stages) {
            // Filter only stages with the relevant delegate. It is not checked later that the
            // delegate exists!
            // If no init/cleanup function exists, implicit activation/deactivation can occur so
            // that dependents/dependencies may activate/deactivate.
            static if (startup) {
                if (v.state == StageState.INACTIVE) {
                    if ((*v).init)
                        toRun.append (v);
                    else
                        v.state = StageState.ACTIVE;
                }
            } else {
                if (v.state == StageState.ACTIVE) {
                    if (v.cleanup)
                        toRun.append (v);
                    else
                        v.state = StageState.INACTIVE;
                }
            }
        }
        // Counts number of active threads, and before threads are started is number to use:
        size_t numWorking = (toRun.size < miscOpts.maxThreads) ? toRun.size : miscOpts.maxThreads;
        enum STATE {    WORKING = 0,    DONE = 1,       ABORT = 2 }
        STATE doneInit = STATE.WORKING;
        Mutex toRunM = new Mutex;       // synchronization on toRun, numWorking
        Condition toRunC = new Condition(toRunM);       // used by threads waiting for remaining stages' dependencies to be met
        
        /* This is a threadable member function to run init stages.
         * Operation follows:
         * 1 Look for a stage to run:
         *   if found:
         *      notify waiting threads work may be available
         *      init stage
         *      goto 1
         *   if not found:
         *      if any other threads are working, wait (dependencies may not be met yet)
         *      if no other threads are working, notify waiting threads and terminate (finished)
         * When notified, threads start at 1. */
        void initThreadFct () {
            try {       // created as a thread - must not throw exceptions
                InitStage* stage;
                
                threadLoop:
                while (true) {      // thread loops until a problem occurs or nothing else can be done
                    // Look for a job:
                    synchronized (toRunM) {
                    --numWorking;           // stopped working: looking/waiting for a job
                    if (doneInit) break threadLoop;  // something went wrong in another thread
                    
                    static if (startup)
                        int num_rdepends = (stage is null) ? 0 : stage.rdepends.length;
                    else
                        int num_rdepends = (stage is null) ? 0 : stage.depends.length;
                    
                    getStage:
                    while (true) {
                        auto toRunIt = toRun.iterator;      // iterates toRun
                        itStages:
                        while (toRunIt.next (stage)) {      // get next element of toRun
                            debug assert (stage !is null, "stage is null");
                            static if (startup) {
                                foreach (d; stage.depends)
                                    if (stages[d].state != StageState.ACTIVE)
                                        continue itStages;  // dependency isn't met (yet)
                            } else {
                                foreach (d; stage.rdepends)
                                    if (stages[d].state != StageState.INACTIVE)
                                        continue itStages;  // reverse dependency isn't unmet (yet)
                            }
                            
                            // All dependencies met
                            debug assert (toRun.size, "toRun is empty (error with iterator)");
                            toRunIt.remove;
                            break getStage;
                        }
                        
                        // No stage remaining with all dependencies met
                        if (toRun.size && numWorking)       // still some working so more dependencies may be met later
                            toRunC.wait;    // wait until another thread finishes
                            else                // no thread is working, so none of what's left is doable, or nothing's left
                                break threadLoop;
                    }
                    ++numWorking;           // got a job!
                    if (num_rdepends > 2)   // how many stages depended on the last one run?
                        toRunC.notifyAll(); // tell all waiting threads there may be work now
                    else if (num_rdepends == 2)
                        toRunC.notify();    // there's potentially work for this thread and one other
                        // else there won't be additional work so don't notify
                    }
                    
                    // Do a job:
                    try {
                        // FIXME - old stage start&finish trace messages - we don't have a name!
                        static if (startup) {
                            debug logger.trace ("InitStage {}: starting init", stage.name);
                            stage.state = (*stage).init();  // init is a property of a pointer (oh no!)
                        } else {
                            debug logger.trace ("InitStage {}: starting cleanup", stage.name);
                            stage.state = stage.cleanup();
                        }
                        debug logger.trace ("InitStage {}: completed; state: {}", stage.name, stage.state);
                    } catch (InitStageException e) {
                        debug logger.trace ("InitStage {}: failed: "~e.msg, stage.name);
                        stage.state = e.state;
                        doneInit = STATE.ABORT;
                        break threadLoop;
                    } catch (Exception e) {
                        debug logger.trace ("InitStage {}: failed: "~e.msg, stage.name);
                        doneInit = STATE.ABORT;
                        break threadLoop;
                    }
                }
            } catch (Exception e) {
                logger.fatal ("Exception in initThreadFct: "~e.msg);
                doneInit = STATE.ABORT;
            }
            doneInit |= STATE.DONE;     // allow other threads a faster exit
            toRunC.notifyAll(); // Most likely if we're exiting, we should make sure others aren't waiting.
            return;
        }
        
        // Start min(miscOpts.maxThreads,toRun.size)-1 threads:
        try {
            ThreadGroup g = new ThreadGroup;
            for (size_t i = numWorking; i > 1; --i)
                g.create (&initThreadFct);
            initThreadFct();    // also run in current thread
            g.joinAll (false);  // don't rethrow exceptions - there SHOULD NOT be any
        } catch (ThreadException e) {
            logger.error ("Exception while using threads: "~e.msg);
            logger.error ("Disabling threads and attempting to continue.");
            miscOpts.set!(int)("NumThreads", 1);        // count includes current thread
            initThreadFct();                            // try with just this thread
        }       // any other exception will be caught in main() and abort program
        
        if (doneInit & STATE.ABORT)
            throw new InitException ("An init/cleanup function failed (see above message(s))");
        
        if (toRun.size)
            foreach (stage; toRun)
                logger.warn ("InitStage {}: was not run due to unmet dependencies", stage.name);
    }
    
    private static {
        Logger logger;
        
        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 {
        auto realInit = stages;         // backup the real init stages
        stages = new typeof(stages);    // an empty test-bed
        
        bool init1, init2, init3 = true;
        StageState s1InitReturns = StageState.ACTIVE;
        addInitStage ("stg1", delegate StageState() {
            init1 = true;
            return s1InitReturns;
        }, delegate StageState() {
            init1 = false;
            return StageState.INACTIVE;
        });
        addInitStage ("stg2", delegate StageState() {
            assert (init1);
            init2 = true;
            return StageState.ACTIVE;
        }, delegate StageState() {
            assert (init1);
            init2 = false;
            return StageState.INACTIVE;
        }, ["stg1"]);
        InitStage s3;
        s3.init = delegate StageState() {
            throw new InitStageException (cast(StageState)7);   // not a normal state, but good for a test
            return StageState.ERROR;
        };
        s3.cleanup = delegate StageState() {
            assert (init1);
            init3 = false;
            return StageState.INACTIVE;
        };
        s3.depends = [ toStageName("stg1") ];
        s3.state = StageState.ACTIVE;   // already active, so s3.init should not run (first time)
        addInitStage ("stg3", &s3);
        
        // Stuff normally done in Init.this():
        // Calculate reverse dependencies of stages:
        foreach (key,stage_p; stages)
            foreach (name; stage_p.depends)
                stages[name].rdepends ~= key;
        auto realMaxThreads = miscOpts.maxThreads;
        miscOpts.set!(int)("maxThreads", 4);    // force up to 4 threads for unittest
        
        logger.level(Logger.Info);              // hide a lot of trace messages
        logger.info ("You should see some warning messages starting \"InitStage\":");
        // Run the above.
        runStages!(true);
        assert (init1);
        assert (init2);
        foreach (s; stages)
            assert (s.state == StageState.ACTIVE);
        
        runStages!(false);
        assert (!init1);
        assert (!init2);
        assert (!init3);
        foreach (s; stages)
            assert (s.state == StageState.INACTIVE);
        
        s1InitReturns = StageState.ERROR;
        // Run again. S2/S3 shouldn't run, S1 won't shut down
        runStages!(true);
        assert (init1);
        assert (!init2);
        assert (!init3);
        runStages!(false);
        assert (init1); // S1 cleanup won't run
        
        stages[toStageName("stg1")].state = StageState.INACTIVE;     // hack it back so we can still test
        s1InitReturns = StageState.ACTIVE;
        init1 = false;
        bool a1 = false;
        try {
            runStages!(true);
            a1 = true;
        } catch (Exception e) {}
        assert (!a1, "runStages didn't throw");
        assert (init1); // s1.init should run first; s2.init may or may not get run
        assert (stages[toStageName("stg3")].state == cast(StageState)7);        // set by the exception
        
        stages = realInit;      // restore the real init stages
        miscOpts.set!(int)("maxThreads", realMaxThreads);
        logger.info ("Unittest complete.");
    }
}