diff mde/scheduler/Init.d @ 20:838577503598

Reworked much of Init. Moved mde.Init to mde.scheduler.Init and largely cleaned up the code. Implemented mde.scheduler.InitStage to reduce dependancies of modules running Init functions. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 22 Mar 2008 16:22:59 +0000
parents
children a60cbb7359dd
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/scheduler/Init.d	Sat Mar 22 16:22:59 2008 +0000
@@ -0,0 +1,305 @@
+/* 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.scheduler.Init;
+
+import mde.scheduler.InitStage;
+import mde.scheduler.exception;
+
+import mde.options;
+import paths = mde.resource.paths;
+import mde.exception;
+
+// tango imports
+import tango.core.Thread;
+import tango.core.Exception;
+import tango.stdc.stringz : fromStringz;
+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;
+}
+
+/**
+ * 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:
+    {
+        // 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
+            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));
+            }
+        }
+        
+        // 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.scheduler.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.trace ("Init: starting");
+        
+        //BEGIN Pre-init (stage init0)
+        /* 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);
+        }
+        
+        // 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 (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 {   // init2
+            cleanupStages ~= &cleanup2; // add appropriate cleanup stage
+            
+            if (runStageThreaded (init2)) runStageForward (init2);
+        }
+        catch (InitStageException) {    // This init stage failed.
+            runCleanupStages();
+            throw new InitException ("Initialisation failed during stage init2");
+        }
+        
+        try {   // init4
+            cleanupStages ~= &cleanup4; // add appropriate cleanup stage
+            
+            if (runStageThreaded (init4)) runStageForward (init4);
+        }
+        catch (InitStageException) {    // This init stage failed.
+            runCleanupStages();
+            throw new InitException ("Initialisation failed during stage init4");
+        }
+        //END Init
+        
+        logger.trace ("Init: done");
+    }
+    
+    /** DTOR - runs cleanup functions. */
+    ~this()
+    {
+        logger.trace ("Cleanup: starting");
+        
+        // cleanup1:
+        Options.save(); // save options... do so here for now
+        
+        // cleanup2, 4:
+        runCleanupStages();
+        
+        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 failure. */
+    
+        const UFE = "Unhandled exception from Init function:";
+        /* Runs all functions consecutively, first-to-last.
+        * If any function fails, halts immediately. */
+        void runStageForward (InitStage s) {
+            foreach (func; s.funcs) {
+                if (s.failure) break;
+                try {
+                    func();
+                } catch (Exception e) {
+                    logger.fatal (UFE);
+                    logger.fatal (e.msg);
+                
+                    s.setFailure();
+                }
+            }
+            if (s.failure) 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 {
+                    func();
+                } catch (Exception e) {
+                    logger.fatal (UFE);
+                    logger.fatal (e.msg);
+                
+                    s.setFailure();
+                }
+            }
+            if (s.failure) 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 (!Options.misc.useThreads) return true;  // Use unthreaded route instead
+        
+            ThreadGroup tg;
+            try {                           // creating/starting threads could fail
+                tg = new ThreadGroup;
+                foreach (func; s.funcs) tg.create(func);  // Start all threads
+            } catch (ThreadException e) {   // Problem with threading; try without threads
+                logger.warn ("Caught ThreadException while trying to create threads:");
+                logger.warn (e.msg);
+                logger.warn ("Will continue in a non-threaded manner.");
+            
+                Options.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);
+                
+                    s.setFailure ();        // abort (but join other threads first)
+                }
+            }
+        
+            if (s.failure) throw new InitStageException;    // Problem running; abort and cleanup from here.
+            return false;                   // Done successfully
+        }
+    }
+    //END runStage...
+    
+    private {
+        InitStage*[] cleanupStages;         // All cleanup stages needing to be run later.
+        void runCleanupStages () {
+            foreach_reverse (s; cleanupStages) {
+                try {
+                    runStageReverse (*s);
+                }
+                catch (InitStageException) {
+                    // We're cleaning up anyway, just report and continue
+                    logger.error ("Cleanup function failed! Continuing...");
+                }
+            }
+            cleanupStages = [];             // All have been run, don't want them getting run again
+        }
+    }
+    
+    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);
+            cleanupUT.addFunc (&cleanupFunc2);
+        }
+        
+        initUT.addFunc (&initFunc);
+        
+        runStageForward (initUT);
+        assert (initialised);
+        
+        runStageReverse (cleanupUT);
+        assert (!initialised);
+
+        logger.info ("Unittest complete.");
+    }
+}