Mercurial > projects > mde
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."); + } +}