comparison mde/setup/Init.d @ 85:56c0ddd90193

Intermediate commit (not stable). Changes to init system.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 11 Sep 2008 11:33:51 +0100
parents 3dfd934100f7
children 79d816b3e2d2
comparison
equal deleted inserted replaced
84:e0f1ec7fe73a 85:56c0ddd90193
12 12
13 You should have received a copy of the GNU General Public License 13 You should have received a copy of the GNU General Public License
14 along with this program. If not, see <http://www.gnu.org/licenses/>. */ 14 along with this program. If not, see <http://www.gnu.org/licenses/>. */
15 15
16 /************************************************************************************************** 16 /**************************************************************************************************
17 * Initialisation setup and exit cleanup module. 17 * Initialisation and cleanup (shutdown) module.
18 * 18 *
19 * This module provides an infrastructure for handling much of the initialisation and 19 * Program startup follows this sequence: static this() functions, pre-init, init.
20 * deinitialisation of the program. It does not, however, provide much of the (de)initialisation 20 * Shutdown consists of: cleanup, post-cleanup, static ~this() functions.
21 * code; with the exception of that for the logger. 21 *
22 * static this and ~this functions should not use any external resources, such as dynamically
23 * loaded libraries or files, and should not interact with other modules. They should be almost
24 * guaranteed not to fail. Preferably, they shouldn't involve large amounts of memory or
25 * processing, but this is not a requirement.
26 *
27 * Pre-init: init code written in this module. Generally only prerequisets of most other stages
28 * go here.
29 *
30 * Init: This is where init code from external modules gets hooked in. Each stage consists of an
31 * initialization function, a corresponding cleanup function, a state, and any dependencies (other
32 * init functions which must be run before this one). Init functions are run according to these
33 * dependencies, potentially threaded simultaeneously with other init functions.
34 *
35 * Cleanup: Cleanup happens similarly to init for all stages requiring shutdown (according to their
36 * state). The init dependencies are applied in reverse order (so if X depended on Y, Y's cleanup
37 * will not run until X's cleanup has completed), and again the functions may be threaded.
38 *
39 * Post-cleanup: like pre-init, this is code written in Init.
22 *************************************************************************************************/ 40 *************************************************************************************************/
23 module mde.setup.Init; 41 module mde.setup.Init;
24 42
25 import mde.setup.init2; // This module is responsible for setting up some init functions. 43 import mde.setup.InitStage; // Controls external delegates run by init
26 import mde.setup.initFunctions;
27 import mde.setup.exception; 44 import mde.setup.exception;
28 45
29 import mde.lookup.Options; 46 import mde.lookup.Options;
30 import paths = mde.setup.paths; 47 import paths = mde.setup.paths;
31 import mde.exception; 48 import mde.exception; // optionsLoadException
32 49
33 // tango imports 50 // tango imports
34 import tango.core.Thread; 51 import tango.core.Thread;
52 import tango.core.sync.Condition;
35 import tango.core.Exception; 53 import tango.core.Exception;
36 import tango.stdc.stringz : fromStringz; 54 import tango.util.container.LinkedList;
55
56 //import tango.stdc.stringz : fromStringz;
37 import tango.io.Console; // for printing command-line usage 57 import tango.io.Console; // for printing command-line usage
38 import TimeStamp = tango.text.convert.TimeStamp, tango.time.WallClock; // output date in log file 58 import TimeStamp = tango.text.convert.TimeStamp, tango.time.WallClock; // output date in log file
39 import tango.util.Arguments; 59 import tango.util.Arguments;
40 import tango.util.log.Log : Log, Logger, Level; 60 import tango.util.log.Log;
41 import tango.util.log.AppendConsole; 61 import tango.util.log.AppendConsole;
42 import tango.util.log.AppendFiles; 62 import tango.util.log.AppendFiles;
43 63
44 // Derelict imports 64 // Derelict imports
45 import derelict.opengl.gl; 65 import derelict.opengl.gl;
46 import derelict.sdl.sdl; 66 import derelict.sdl.sdl;
47 //import derelict.sdl.image; Was to be used... for now isn't 67 //import derelict.sdl.image; Was to be used... for now isn't
48 import derelict.freetype.ft; 68 import derelict.freetype.ft;
49 import derelict.util.exception; 69 import derelict.util.exception;
50 70
51 /** 71
52 * Static CTOR 72 /**************************************************************************************************
53 *
54 * This should handle a minimal amount of functionality where useful. For instance, configuring the
55 * logger here and not in Init allows unittests to use the logger.
56 */
57 static this()
58 {
59 Logger root = Log.root;
60 // Set the level here, but set it again once options have been loaded:
61 debug root.level(Logger.Trace);
62 else root.level(Logger.Info);
63 // Temporarily log to the console (until we've found paths and loaded options):
64 root.add(new AppendConsole);
65 }
66 static ~this()
67 {
68 }
69
70 /**
71 * Init class 73 * Init class
72 * 74 *
73 * A scope class created at beginning of the program and destroyed at the end; thus the CTOR 75 * A scope class created at beginning of the program and destroyed at the end; thus the CTOR
74 * handles program initialisation and the DTOR handles program cleanup. 76 * handles program initialisation and the DTOR handles program cleanup.
75 */ 77 *************************************************************************************************/
76 scope class Init 78 scope class Init
77 { 79 {
78 //private bool failure = false; // set true on failure during init, so that clean
79 private static Logger logger;
80 static this() { 80 static this() {
81 // Set up the logger temporarily (until pre-init):
82 Logger root = Log.root;
83 debug root.level(Logger.Trace);
84 else root.level(Logger.Info);
85 root.add(new AppendConsole);
86
81 logger = Log.getLogger ("mde.setup.Init"); 87 logger = Log.getLogger ("mde.setup.Init");
82 } 88 }
83 89
84 /** this() − initialisation 90 /** this() − pre-init and init */
85 *
86 * Runs general initialisation code, in a threaded manner where this isn't difficult.
87 * If any init fails, cleanup is still handled by ~this().
88 *
89 * Init order: 1. Pre-init (loads components needed by most init functions). 2. Dynamic library
90 * loading (load any dynamic libraries first, so that if loading fails nothing else need be
91 * done). 3. Init functions (threaded functions handling the rest of initialisation).
92 */
93 /* In a single-threaded function this could be done with:
94 * scope(failure) cleanup;
95 * This won't work with a threaded init function since any threads completing succesfully will
96 * not clean-up, and a fixed list of clean-up functions cannot be used since clean-up functions
97 * must not run where the initialisation functions have failed.
98 * Hence a list of clean-up functions is built similarly to scope(failure) --- see addCleanupFct.
99 */
100 this(char[][] cmdArgs) 91 this(char[][] cmdArgs)
101 { 92 {
102 debug logger.trace ("Init: starting"); 93 /******************************************************************************************
103 94 * Pre-init - init code written in this module.
104 //BEGIN Pre-init (stage init0) 95 *****************************************************************************************/
96 debug logger.trace ("Init: starting pre-init");
105 //FIXME: warn on invalid arguments, including base-path on non-Windows 97 //FIXME: warn on invalid arguments, including base-path on non-Windows
106 // But Arguments doesn't support this (in tango 0.99.6 and in r3563). 98 // But Arguments doesn't support this (in tango 0.99.6 and in r3563).
107 Arguments args; 99 Arguments args;
108 try { 100 try {
109 args = new Arguments(); 101 args = new Arguments();
208 200
209 throw new InitException ("Loading dynamic libraries failed (see above)."); 201 throw new InitException ("Loading dynamic libraries failed (see above).");
210 } 202 }
211 debug logger.trace ("Init: dynamic libraries loaded"); 203 debug logger.trace ("Init: dynamic libraries loaded");
212 //END Load dynamic libraries 204 //END Load dynamic libraries
213 //END Pre-init 205
214 206
215 207 /******************************************************************************************
216 //BEGIN Init (stages init2, init4) 208 * Init − where init code from external modules gets hooked in.
217 /* Call init functions. 209 *****************************************************************************************/
218 * 210 debug logger.trace ("Init: done pre-init, starting init stages");
219 * Current method is to try using threads, and on failure assume no threads were actually 211
220 * created and run functions in a non-threaded manner. */ 212 // Calculate reverse dependencies of stages:
221 213 foreach (key,stage_p; stages)
222 try { 214 foreach (name; stage_p.depends)
223 if (runStageThreaded (init)) runStageForward (init); 215 stages[name].rdepends ~= key;
224 } 216 if (miscOpts.numThreads < 1 || miscOpts.numThreads > 64) // limit to a sensible number of threads
225 catch (InitStageException) { // This init stage failed. 217 miscOpts.set!(int)("numThreads", 4); // FIXME enforce limit in Options
226 // FIXME: check DTOR still runs 218
227 throw new InitException ("An init function failed (see above message(s))"); 219 runStages!(true); // startup delegates
228 }
229 //END Init
230 220
231 debug logger.trace ("Init: done"); 221 debug logger.trace ("Init: done");
232 } 222 }
233 223
234 /** DTOR - runs cleanup functions. */ 224 /** DTOR - runs cleanup functions. */
235 ~this() 225 ~this()
236 { 226 {
237 debug logger.trace ("Cleanup: starting"); 227 debug logger.trace ("Cleanup: starting");
238 228
229 runStages!(false); // cleanup delegates
230
239 Options.save(); // save options... do so here for now 231 Options.save(); // save options... do so here for now
240 232
241 // General cleanup: 233 debug logger.trace ("Cleanup: done");
234 }
235
236 // run init stages or cleanup if startup is false
237 private static void runStages(bool startup) () {
238 LinkedList!(InitStage*) toRun = new LinkedList!(InitStage*);
239 foreach (v; stages) {
240 // Filter only stages with the relevant delegate. It is not checked later that the
241 // delegate exists!
242 // If no init/cleanup function exists, implicit activation/deactivation can occur so
243 // that dependents/dependencies may activate/deactivate.
244 static if (startup) {
245 if (v.state == StageState.INACTIVE) {
246 if ((*v).init)
247 toRun.append (v);
248 else
249 v.state = StageState.ACTIVE;
250 }
251 } else {
252 if (v.state == StageState.ACTIVE) {
253 if (v.cleanup)
254 toRun.append (v);
255 else
256 v.state = StageState.INACTIVE;
257 }
258 }
259 }
260 debug logger.trace ("Added {} init/cleanup stages", toRun.size);
261 int numWorking = miscOpts.numThreads;
262 bool abortInit = false;
263 Mutex toRunM = new Mutex; // synchronization on toRun, numWorking
264 Condition toRunC = new Condition(toRunM); // used by threads waiting for remaining stages' dependencies to be met
265
266 /* This is a threadable member function to run init stages.
267 * Operation follows:
268 * 1 Look for a stage to run:
269 * if found:
270 * notify waiting threads work may be available
271 * init stage
272 * goto 1
273 * if not found:
274 * if any other threads are working, wait (dependencies may not be met yet)
275 * if no other threads are working, notify waiting threads and terminate (finished)
276 * When notified, threads start at 1. */
277 void initThreadFct () {
278 debug logger.trace ("initThreadFct: starting");
279 try { // created as a thread - must not throw exceptions
280 InitStage* stage;
281
282 threadLoop: while (true) { // thread loops until a problem occurs or nothing else can be done
283 // Look for a job:
284 synchronized (toRunM) {
285 --numWorking; // stopped working: looking/waiting for a job
286 if (abortInit) break threadLoop; // something went wrong in another thread
287 static if (startup)
288 int num_rdepends = (stage is null) ? 0 : stage.rdepends.length;
289 else
290 int num_rdepends = (stage is null) ? 0 : stage.depends.length;
291
292 getStage: while (true) {
293 auto it = toRun.iterator; // asserts if toRun is empty
294 itStages: while (it.next (stage)) { // get next element of toRun
295 static if (startup) {
296 foreach (d; stage.depends)
297 if (stages[d].state != StageState.ACTIVE)
298 continue itStages; // dependency isn't met (yet)
299 } else {
300 foreach (d; stage.rdepends)
301 if (stages[d].state != StageState.INACTIVE)
302 continue itStages; // reverse dependency isn't unmet (yet)
303 }
304
305 // All dependencies met
306 it.remove;
307 break getStage;
308 }
309
310 // No stage remaining with all dependencies met
311 if (!toRun.isEmpty && numWorking) // still some working so more dependencies may be met later
312 toRunC.wait; // wait until another thread finishes
313 else // no thread is working, so none of what's left is doable, or nothing's left
314 break threadLoop;
315 }
316 ++numWorking; // got a job!
317 if (num_rdepends > 2) // how many stages depended on the last one run?
318 toRunC.notifyAll(); // tell all waiting threads there may be work now
319 else if (num_rdepends == 2)
320 toRunC.notify(); // there's potentially work for this thread and one other
321 // else there won't be additional work so don't notify
322 }
323
324 // Do a job:
325 try {
326 // FIXME - old stage start&finish trace messages - we don't have a name!
327 debug logger.trace ("InitStage {}: starting", stage);
328 static if (startup)
329 stage.state = (*stage).init(); // init is a property of a pointer (oh no!)
330 else
331 stage.state = stage.cleanup();
332 debug logger.trace ("InitStage {}: completed; state: {}", stage, stage.state);
333 } catch (InitStageException e) {
334 debug logger.trace ("InitStage {}: failed", stage);
335 stage.state = e.state;
336 abortInit = true;
337 break threadLoop;
338 } catch (Exception e) {
339 debug logger.trace ("InitStage {}: failed", stage);
340 abortInit = true;
341 break threadLoop;
342 }
343 }
344 } catch (Exception e) {
345 logger.fatal ("Exception in initThreadFct: "~e.msg);
346 abortInit = true;
347 }
348 toRunC.notifyAll(); // Most likely if we're exiting, we should make sure others aren't waiting.
349 debug logger.trace ("initThreadFct: returning");
350 return;
351 }
352
353 // Start miscOpts.NumThreads - 1 threads:
242 try { 354 try {
243 if (runStageThreaded (cleanup)) runStageReverse (cleanup); 355 ThreadGroup g = new ThreadGroup;
244 } 356 for (int i = miscOpts.numThreads; i > 1; --i)
245 catch (InitStageException) { 357 g.create (&initThreadFct);
246 // Nothing else to do but report: 358 initThreadFct(); // also run in current thread
247 logger.error ("One or more cleanup functions failed!"); 359 g.joinAll (false); // don't rethrow exceptions - there SHOULD NOT be any
248 } 360 } catch (ThreadException e) {
249 361 logger.error ("Exception while using threads: "~e.msg);
250 debug logger.trace ("Cleanup: done"); 362 logger.error ("Disabling threads and attempting to continue.");
251 } 363 miscOpts.set!(int)("NumThreads", 1); // count includes current thread
252 364 initThreadFct(); // try with just this thread
253 365 } // any other exception will be caught in main() and abort program
254 //BEGIN runStage... 366
367 if (abortInit)
368 throw new InitException ("An init/cleanup function failed (see above message(s))");
369
370 foreach (stage; toRun)
371 logger.warn ("InitStage {}: was not run due to unmet dependencies", stage);
372 }
373
255 private static { 374 private static {
256 /* The following three functions, runStage*, each run all functions in a stage in some order, 375 Logger logger;
257 * catching any exceptions thrown by the functions (although this isn't guaranteed for threads),
258 * and throw an InitStageException on initFailure. */
259
260 const LOG_IF_MSG = "Init function ";
261 const LOG_CF_MSG = "Cleanup function ";
262 const LOG_F_START = " - running";
263 const LOG_F_END = " - completed";
264 const LOG_F_BAD = " - failed";
265 const LOG_F_FAIL = " - exception: ";
266 /* Runs all functions consecutively, first-to-last.
267 * If any function fails, halts immediately. */
268 void runStageForward (InitStage s) {
269 foreach (func; s.funcs) {
270 if (initFailure) break;
271 try {
272 debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START);
273 func.func();
274 debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
275 } catch (Exception e) {
276 logger.fatal (LOG_IF_MSG ~ func.name ~ LOG_F_FAIL ~
277 ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) );
278
279 setInitFailure();
280 }
281 }
282
283 if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here.
284 }
285 /* Runs all functions consecutively, last-to-first.
286 * If any function fails, continue until all have been run. */
287 void runStageReverse (InitStage s) {
288 foreach_reverse (func; s.funcs) {
289 try {
290 debug logger.trace (LOG_CF_MSG ~ func.name ~ LOG_F_START);
291 func.func();
292 debug logger.trace (LOG_CF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
293 } catch (Exception e) {
294 logger.fatal (LOG_CF_MSG ~ func.name ~ LOG_F_FAIL ~
295 ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) );
296
297 setInitFailure();
298 }
299 }
300 if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here.
301 }
302 /* Tries running functions in a threaded way. Returns false if successful, true if not but
303 * functions should be run without threads. */
304 bool runStageThreaded (InitStage s) {
305 if (!miscOpts.useThreads) return true; // Use unthreaded route instead
306
307 ThreadGroup tg;
308 try { // creating/starting threads could fail
309 tg = new ThreadGroup;
310 foreach (func; s.funcs) { // Start all threads
311 debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START);
312 tg.create(func.func);
313 debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
314 }
315 } catch (ThreadException e) { // Problem with threading; try without threads
316 logger.error ("Caught ThreadException while trying to create threads:");
317 logger.error (e.msg);
318 logger.info ("Will disable threads and continue, assuming no threads were created.");
319
320 miscOpts.set!(bool)("useThreads", false); // Disable threads entirely
321 return true; // Try again without threads
322 }
323
324 /* Wait for all threads to complete.
325 *
326 * If something went wrong, we still need to do this before cleaning up.
327 */
328 foreach (t; tg) {
329 try {
330 t.join (true);
331 } catch (Exception e) {
332 // Relying on catching exceptions thrown by other threads is a bad idea.
333 // Hence all threads should catch their own exceptions and return safely.
334
335 logger.fatal ("Unhandled exception from Init function:");
336 logger.fatal (e.msg);
337
338 setInitFailure (); // abort (but join other threads first)
339 }
340 }
341
342 if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here.
343 return false; // Done successfully
344 }
345 //END runStage...
346 376
347 void printUsage (char[] progName) { 377 void printUsage (char[] progName) {
348 Cout ("mde [no version]").newline; 378 Cout ("mde [no version]").newline;
349 Cout ("Usage:").newline; 379 Cout ("Usage:").newline;
350 Cout (progName ~ ` [options]`).newline; 380 Cout (progName ~ ` [options]`).newline;
364 --help, -h Print this message.`).newline; 394 --help, -h Print this message.`).newline;
365 } 395 }
366 } 396 }
367 397
368 debug (mdeUnitTest) unittest { 398 debug (mdeUnitTest) unittest {
369 /* Fake init and cleanup. Use unittest-specific init and cleanup InitStages to avoid 399 logger.trace ("Starting unittest");
370 * messing other init/cleanup up. */ 400
371 static InitStage initUT, cleanupUT; 401 auto realInit = stages; // backup the real init stages
372 402 stages = new typeof(stages); // an empty test-bed
373 static bool initialised = false; 403
374 static void cleanupFunc1 () { 404 bool init1, init2, init3 = true;
375 initialised = false; 405 bool failed = false; // set if anything goes wrong
376 } 406 StageState s1InitReturns = StageState.ACTIVE;
377 static void cleanupFunc2 () { 407 addInitStage ("stg1", delegate StageState() {
378 assert (initialised == true); 408 init1 = true;
379 } 409 return s1InitReturns;
380 410 }, delegate StageState() {
381 static void initFunc () { 411 init1 = false;
382 initialised = true; 412 return StageState.INACTIVE;
383 cleanupUT.addFunc (&cleanupFunc1, "UT cleanup 1"); 413 });
384 cleanupUT.addFunc (&cleanupFunc2, "UT cleanup 2"); 414 addInitStage ("stg2", delegate StageState() {
385 } 415 if (!init1) failed = true;
386 416 init2 = true;
387 initUT.addFunc (&initFunc, "UT init"); 417 return StageState.ACTIVE;
388 418 }, delegate StageState() {
389 runStageForward (initUT); 419 if (!init1) failed = true;
390 assert (initialised); 420 init2 = false;
391 421 return StageState.INACTIVE;
392 runStageReverse (cleanupUT); 422 }, ["stg1"]);
393 assert (!initialised); 423 InitStage s3;
394 424 s3.init = delegate StageState() {
425 throw new InitStageException (cast(StageState)7); // not a normal state, but good for a test
426 return StageState.ERROR;
427 };
428 s3.cleanup = delegate StageState() {
429 if (!init1) failed = true;
430 init3 = false;
431 return StageState.INACTIVE;
432 };
433 s3.depends = [ toStageName("stg1") ];
434 s3.state = StageState.ACTIVE; // already active, so s3.init should not run (first time)
435 addInitStage ("stg3", &s3);
436
437 // Stuff normally done in Init.this():
438 // Calculate reverse dependencies of stages:
439 foreach (key,stage_p; stages)
440 foreach (name; stage_p.depends)
441 stages[name].rdepends ~= key;
442 if (miscOpts.numThreads < 1 || miscOpts.numThreads > 64) // limit to a sensible number of threads
443 miscOpts.set!(int)("numThreads", 4); // FIXME enforce limit in Options
444
445
446 // Run the above.
447 runStages!(true);
448 assert (init1);
449 assert (init2);
450 assert (!failed);
451 foreach (s; stages)
452 assert (s.state == StageState.ACTIVE);
453
454 runStages!(false);
455 assert (!init1);
456 assert (!init2);
457 assert (!init3);
458 assert (!failed);
459 foreach (s; stages)
460 assert (s.state == StageState.INACTIVE);
461
462 s1InitReturns = StageState.ERROR;
463 // Run again. S2/S3 shouldn't run, S1 won't shut down
464 runStages!(true);
465 assert (init1);
466 assert (!init2);
467 assert (!init3);
468 assert (!failed);
469 runStages!(false);
470 assert (init1); // S1 cleanup won't run
471
472 stages[toStageName("stg1")].state = StageState.INACTIVE; // hack it back so we can still test
473 s1InitReturns = StageState.ACTIVE;
474 init1 = false;
475 try {
476 runStages!(true);
477 assert (false); // runStages should throw because s3.init runs now
478 } catch (Exception) {}
479 assert (init1); // s1.init should run first
480 assert (stages[toStageName("stg3")].state == cast(StageState)7); // set by the exception
481
482 stages = realInit; // restore the real init stages
395 logger.info ("Unittest complete."); 483 logger.info ("Unittest complete.");
396 } 484 }
397 } 485 }