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