diff 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 diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/setup/Init.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,407 @@
+/* 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.");
+    }
+}