Mercurial > projects > mde
view mde/setup/Init.d @ 173:a1ba9157510e
Enabled ServiceContentList to call its callbacks when its value changes. Tried to fix some other bugs, but this is not a very clean commit, due to wanting to make some big changes to enable better use of invariants next.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Sat, 08 Aug 2009 15:53:10 +0200 |
parents | e45226d3deae |
children | 62aa8845edd2 |
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.setup.logger; import mde.content.AStringContent; import mde.content.ContentLoader; import paths = mde.file.paths; import mde.exception; // optionsLoadException import imde = mde.imde; // tango imports import tango.core.Thread; import tango.core.sync.Condition; import tango.core.Exception; import tango.util.container.LinkedList; //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.ArgParser; 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() { logger = Log.getLogger ("mde.setup.Init"); exitImmediately = new BoolContent ("MiscOptions.exitImmediately"); maxThreads = new IntContent ("MiscOptions.maxThreads"); logLevel = new EnumContent ("MiscOptions.logLevel", ["Trace", "Info", "Warn", "Error", "Fatal", "None"]); logOutput = new EnumContent ("MiscOptions.logOutput", ["both", "file", "console", "none"]); // Callback to set the logging level on change: logLevel.addCallback (&setLogLevel); } /** this() − pre-init and init */ this(char[][] cmdArgs) { /********************************************************************** * Pre-init - init code written in this module. *********************************************************************/ debug logger.trace ("Init: starting pre-init"); try { // Create without a default-argument delegate; let ArgParser throw: auto args = new ArgParser (); char[] basePath = "."; bool printPaths = false; args.bind("--", "base-path=", delegate void(char[] value){ basePath=value; }); args.bind("--", "data-path=", delegate void(char[] value){ paths.extraDataPath = value; }); args.bind("--", "conf-path=", delegate void(char[] value){ paths.extraConfPath = value; }); args.bind("--", "font-path=", delegate void(char[] value){ paths.addFontPath (value); }); args.bind("--", "paths", delegate void(char[]){ printPaths = true; }); args.bind([Argument("--", "quick-exit"), Argument ("-", "q")], delegate void(char[]){ imde.run = false; }); args.bind([Argument("--", "help"), Argument ("-", "h")], delegate void(char[]){ printUsage(cmdArgs[0]); // Requesting help is an "error" in that normal program operation is cut short. throw new InitException ("Help requested"); // stops program }); args.parse(cmdArgs[1..$]); paths.resolvePaths (basePath); if (printPaths) { paths.mdeDirectory.printPaths; throw new InitException ("Paths requested"); // lazy way to stop } } catch (Exception e) { throw new InitException ("Command-line: "~e.msg); } 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 { ContentLoader.load(); } catch (Exception e) { throw new InitException ("Loading options (content values) failed: " ~ e.msg); } debug logger.trace ("Init: loaded options successfully"); // Set up the logger: Logger root; try { // 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 // logOutput == 0 enables both outputs, in case options aren't read if (!(logOutput() & 2)) { // first appender so root seperator messages don't show on console // Use 2 log files with a maximum size of 16kiB: root.add (new AppendFiles (paths.logDir~"/log-.txt", 2, 16*1024)); root.append (Level.None, ""); // some kind of separation between runs root.append (Level.None, ""); } if (!(logOutput() & 1)) root.add(new AppendConsole); } 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.error ("Exception while setting up the logger; logging to the console instead."); } // Done enough init to know where and when to log messages now logger.info ("Starting mde [no version] on " ~ TimeStamp.toString(WallClock.now)); // a debugging option: imde.run = imde.run && !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; runStages!(true); // startup delegates debug logger.trace ("Init: done"); } /** DTOR - runs cleanup functions. */ ~this() { debug logger.trace ("Cleanup: starting"); runStages!(false); // cleanup delegates ContentLoader.save(); // save options before exiting debug logger.trace ("Cleanup: done"); } // run init stages or cleanup if startup is false private static void runStages(bool startup) () { auto toRun = new LinkedList!(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; } } } // Counts number of active threads, and before threads are started is number to use: size_t numWorking = (toRun.size < maxThreads()) ? toRun.size : maxThreads(); 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 /* initThreadFct is now in a class to allow reliably identifying whcich instance is run in the main thread. * The main thread gets index 0, other threads indexes 2,3,4,etc. (there is no 1). */ class InitStageThread : Thread { this (int n) { debug threadNum = n; super (&initThreadFct); } debug int threadNum; /* 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) { auto toRunIt = toRun.iterator; // iterates toRun itStages: while (toRunIt.next (stage)) { // get next element of toRun debug assert (stage !is null, "stage is null"); 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 { static if (startup) { debug logger.trace ("({}) InitStage {}: starting init", threadNum, stage.name); stage.state = (*stage).init(); // init is a property too :-( } else { debug logger.trace ("({}) InitStage {}: starting cleanup", threadNum, stage.name); stage.state = stage.cleanup(); } debug logger.trace ("({}) InitStage {}: completed; state: {}", threadNum, stage.name, stage.state); } catch (InitStageException e) { debug logger.error ("({}) InitStage {}: failed: "~e.msg, threadNum, stage.name); else logger.error ("InitStage {}: failed: "~e.msg, stage.name); e.writeOut(delegate void(char[]s){ Cerr(s); }); stage.state = e.state; doneInit = STATE.ABORT; break threadLoop; } catch (Exception e) { debug logger.error ("({}) InitStage {}: failed: "~e.msg, threadNum, stage.name); else logger.error ("InitStage {}: failed: "~e.msg, stage.name); e.writeOut(delegate void(char[]s){ Cerr(s); }); 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 min(miscOpts.maxThreads,toRun.size)-1 threads: try { ThreadGroup g = new ThreadGroup; InitStageThread x; for (size_t i = numWorking; i > 1; --i) { //g.create (&initThreadFct); x = new InitStageThread (i); x.start; g.add (x); } x = new InitStageThread (0); x.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."); maxThreads = 1; // count includes current thread auto x = new InitStageThread (0); x.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.name); } private static { Logger logger; BoolContent exitImmediately; IntContent maxThreads; EnumContent logLevel, logOutput; // Callback on logLevel void setLogLevel (IContent = null) { int level = logLevel(); if (level < Level.Trace || level > Level.None) { logger.error ("incorrect logging level"); level = Level.Info; return; // setting the level causes this function to be called again } debug { Log.root.level (Level.Trace); logger.trace ("Setting logging level {}", logLevel()); } Log.root.level (logOutput() == 3 ? Level.None : cast(Level) logLevel(), true); } 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 Add path as a potential location for data files. May be given multiple times. First path argument becomes the prefered location to load data files from. --conf-path=path Add path as a potential location for config files. May be given multiple times. 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; int realMaxThreads = maxThreads(); maxThreads = 4; // force up to 4 threads for unittest logger.level(Logger.Info); // hide a lot of trace messages logger.info ("You should see some messages about InitStages not run/failing:"); // 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 maxThreads = realMaxThreads; logger.info ("Unittest complete."); } }