Mercurial > projects > mde
view mde/setup/Init.d @ 88:01f4f5f1acc9
Changes to init and to allow compiling with gdc.
Tweaked init code to allow using circular iterators (disabled until my patch makes it into tango).
Changes to allow compiling with gdc. Building is successful and unittests complete, but in my experience a SIGSEGV occurs within SDL.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Mon, 29 Sep 2008 12:09:44 +0100 |
parents | 79d816b3e2d2 |
children | 97e6dce08037 |
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.lookup.Options; import paths = mde.setup.paths; import mde.exception; // optionsLoadException // tango imports import tango.core.Thread; import tango.core.sync.Condition; import tango.core.Exception; import tango.util.container.CircularList; //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; 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() { // Set up the logger temporarily (until pre-init): Logger root = Log.root; debug root.level(Logger.Trace); else root.level(Logger.Info); root.add(new AppendConsole); logger = Log.getLogger ("mde.setup.Init"); } /** this() − pre-init and init */ this(char[][] cmdArgs) { /****************************************************************************************** * Pre-init - init code written in this module. *****************************************************************************************/ debug logger.trace ("Init: starting pre-init"); //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.root; root.clear; // we may no longer want to log to the console // Now re-set the logging level, using the value from the config file: root.level (cast(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) { // Use 2 log files with a maximum size of 1 MB: root.add (new AppendFiles (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 miscOpts.set!(int) ("logOptions", miscOpts.logOptions | LOG.CONSOLE); } if (miscOpts.logOptions & LOG.CONSOLE) { // Log to the console 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.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 /****************************************************************************************** * 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; if (miscOpts.numThreads < 1 || miscOpts.numThreads > 64) // limit to a sensible number of threads miscOpts.set!(int)("numThreads", 4); // FIXME enforce limit in Options runStages!(true); // startup delegates debug logger.trace ("Init: done"); } /** DTOR - runs cleanup functions. */ ~this() { debug logger.trace ("Cleanup: starting"); runStages!(false); // cleanup delegates Options.save(); // save options... do so here for now debug logger.trace ("Cleanup: done"); } // run init stages or cleanup if startup is false private static void runStages(bool startup) () { CircularList!(InitStage*) toRun = new CircularList!(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; } } } auto toRunIt = toRun.iterator; int numWorking = miscOpts.numThreads; 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 /* 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) { static if (false) // An addition to CircularList to allow continuous circular iteration; not yet in tango. toRunIt.start; // start circling from here else toRunIt = toRun.iterator; // new iterator itStages: while (toRunIt.next (stage)) { // get next element of toRun debug logger.trace ("Iterating: {}", stage); 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 { // FIXME - old stage start&finish trace messages - we don't have a name! debug logger.trace ("InitStage {}: starting", stage); static if (startup) stage.state = (*stage).init(); // init is a property of a pointer (oh no!) else stage.state = stage.cleanup(); debug logger.trace ("InitStage {}: completed; state: {}", stage, stage.state); } catch (InitStageException e) { debug logger.trace ("InitStage {}: failed", stage); stage.state = e.state; doneInit = STATE.ABORT; break threadLoop; } catch (Exception e) { debug logger.trace ("InitStage {}: failed", stage); 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 miscOpts.NumThreads - 1 threads: try { ThreadGroup g = new ThreadGroup; for (int i = miscOpts.numThreads; i > 1; --i) g.create (&initThreadFct); 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."); miscOpts.set!(int)("NumThreads", 1); // count includes current thread 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); } private static { Logger logger; 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 { 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; if (miscOpts.numThreads < 1 || miscOpts.numThreads > 64) // limit to a sensible number of threads miscOpts.set!(int)("numThreads", 4); // FIXME enforce limit in Options // 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 logger.info ("Unittest complete."); } }