view mde/setup/Init.d @ 151:e785e98d3b78

Updated for compatibility with tango 0.99.8.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 04 Apr 2009 17:32:18 +0200
parents 9f035cd139c6
children 2934fcacbb97
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.setup.logger;

import mde.content.AStringContent;
import mde.content.ContentLoader;
import paths = mde.file.paths;
import mde.exception;           // optionsLoadException
import imde = mde.imde;

// 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.ArgParser;
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() {
        logger = Log.getLogger ("mde.setup.Init");
        exitImmediately = new BoolContent ("MiscOptions.exitImmediately");
        maxThreads = new IntContent ("MiscOptions.maxThreads");
        logLevel = new EnumContent ("MiscOptions.logLevel",
                                    ["Trace", "Info", "Warn", "Error", "Fatal", "None"]);
        logOutput = new EnumContent ("MiscOptions.logOutput",
                                     ["both", "file", "console", "none"]);
	
	// Callback to set the logging level on change:
	logLevel.addCallback (&setLogLevel);
    }
    
    /** this() − pre-init and init */
    this(char[][] cmdArgs)
    {
        /**********************************************************************
         * Pre-init - init code written in this module.
         *********************************************************************/
        debug logger.trace ("Init: starting pre-init");
        try {
	    // Create without a default-argument delegate; let ArgParser throw:
	    auto args = new ArgParser ();
	    char[] basePath = ".";
	    bool printPaths = false;
	    args.bind("--", "base-path=", delegate void(char[] value){
		basePath=value;
	    });
	    args.bind("--", "data-path=", delegate void(char[] value){
		paths.extraDataPath = value;
	    });
	    args.bind("--", "conf-path=", delegate void(char[] value){
		paths.extraConfPath = value;
	    });
	    args.bind("--", "font-path=", delegate void(char[] value){
		paths.addFontPath (value);
	    });
	    args.bind("--", "paths", delegate void(char[]){
		printPaths = true;
	    });
            args.bind([Argument("--", "quick-exit"), Argument ("-", "q")],
		      delegate void(char[]){
		imde.run = false;
	    });
            args.bind([Argument("--", "help"), Argument ("-", "h")],
		      delegate void(char[]){
		printUsage(cmdArgs[0]);
		// Requesting help is an "error" in that normal program operation is cut short.
		throw new InitException ("Help requested");	// stops program
	    });
            args.parse(cmdArgs[1..$]);
	    paths.resolvePaths (basePath);
	    
	    if (printPaths) {
		paths.mdeDirectory.printPaths;
		throw new InitException ("Paths requested");	// lazy way to stop
	    }
        } catch (Exception e) {
            throw new InitException ("Command-line: "~e.msg);
        }
        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 {
            ContentLoader.load();
        } catch (Exception e) {
            throw new InitException ("Loading options (content values) failed: " ~ e.msg);
        }
        debug logger.trace ("Init: loaded options successfully");
        
	// Set up the logger:
        Logger root;
	try {
	    // 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
	    
	    // logOutput == 0 enables both outputs, in case options aren't read
            if (!(logOutput() & 2)) {     // first appender so root seperator messages don't show on console
                // Use 2 log files with a maximum size of 16kiB:
                root.add (new AppendFiles (paths.logDir~"/log-.txt", 2, 16*1024));
                root.append (Level.None, ""); // some kind of separation between runs
                root.append (Level.None, "");
            }
            if (!(logOutput() & 1))
                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.error ("Exception while setting up the logger; logging to the console instead.");
        }
        
        // a debugging option:
        imde.run = imde.run && !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
        
        ContentLoader.save();	// save options before exiting
        
        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 < maxThreads()) ? toRun.size : 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
        
        /* initThreadFct is now in a class to allow reliably identifying whcich instance is run in the main thread.
         * The main thread gets index 0, other threads indexes 2,3,4,etc. (there is no 1). */
        class InitStageThread : Thread {
            this (int n) {
                debug threadNum = n;
                super (&initThreadFct);
            }
            debug int threadNum;
        /* 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 {
                        static if (startup) {
                            debug logger.trace ("({}) InitStage {}: starting init", threadNum, stage.name);
                            stage.state = (*stage).init();  // init is a property too :-(
                        } else {
                            debug logger.trace ("({}) InitStage {}: starting cleanup", threadNum, stage.name);
                            stage.state = stage.cleanup();
                        }
                        debug logger.trace ("({}) InitStage {}: completed; state: {}", threadNum, stage.name, stage.state);
                    } catch (InitStageException e) {
                        debug logger.error ("({}) InitStage {}: failed: "~e.msg, threadNum, stage.name);
                        else logger.error ("InitStage {}: failed: "~e.msg, stage.name);
                        stage.state = e.state;
                        doneInit = STATE.ABORT;
                        break threadLoop;
                    } catch (Exception e) {
                        debug logger.error ("({}) InitStage {}: failed: "~e.msg, threadNum, stage.name);
                        else logger.error ("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;
            InitStageThread x;
            for (size_t i = numWorking; i > 1; --i) {
                //g.create (&initThreadFct);
                x = new InitStageThread (i);
                x.start;
                g.add (x);
            }
            x = new InitStageThread (0);
            x.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.");
            maxThreads = 1;    // count includes current thread
            auto x = new InitStageThread (0);
            x.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;
        BoolContent exitImmediately;
        IntContent maxThreads;
        EnumContent logLevel, logOutput;
        
        // Callback on logLevel
        void setLogLevel (Content = null) {
	    int level = logLevel();
	    if (level < Level.Trace || level > Level.None) {
		logger.error ("incorrect logging level");
		level = Level.Info;
		return;	// setting the level causes this function to be called again
	    }
	    debug {
		Log.root.level (Level.Trace);
		logger.trace ("Setting logging level {}", logLevel());
	    }
            Log.root.level (logOutput() == 0 ? Level.None : cast(Level) logLevel(), true);
        }
        
        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	Add path as a potential location for data files. May be
			given multiple times. First path argument becomes
			the prefered location to load data files from.
  --conf-path=path	Add path as a potential location for config files. May
			be given multiple times. 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;
        int realMaxThreads = maxThreads();
        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 messages about InitStages not run/failing:");
        // 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
        maxThreads = realMaxThreads;
        logger.info ("Unittest complete.");
    }
}