Mercurial > projects > mde
comparison 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 |
comparison
equal
deleted
inserted
replaced
62:960206198cbd | 63:66d555da083e |
---|---|
1 /* LICENSE BLOCK | |
2 Part of mde: a Modular D game-oriented Engine | |
3 Copyright © 2007-2008 Diggory Hardy | |
4 | |
5 This program is free software: you can redistribute it and/or modify it under the terms | |
6 of the GNU General Public License as published by the Free Software Foundation, either | |
7 version 2 of the License, or (at your option) any later version. | |
8 | |
9 This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; | |
10 without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
11 See the GNU General Public License for more details. | |
12 | |
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/>. */ | |
15 | |
16 /************************************************************************************************** | |
17 * Initialisation setup and exit cleanup module. | |
18 * | |
19 * This module provides an infrastructure for handling much of the initialisation and | |
20 * deinitialisation of the program. It does not, however, provide much of the (de)initialisation | |
21 * code; with the exception of that for the logger. | |
22 *************************************************************************************************/ | |
23 module mde.setup.Init; | |
24 | |
25 import mde.setup.init2; // This module is responsible for setting up some init functions. | |
26 import mde.setup.initFunctions; | |
27 import mde.setup.exception; | |
28 | |
29 import mde.lookup.Options; | |
30 import paths = mde.setup.paths; | |
31 import mde.exception; | |
32 | |
33 // tango imports | |
34 import tango.core.Thread; | |
35 import tango.core.Exception; | |
36 import tango.stdc.stringz : fromStringz; | |
37 import tango.io.Console; // for printing command-line usage | |
38 import TimeStamp = tango.text.convert.TimeStamp, tango.time.WallClock; // output date in log file | |
39 import tango.util.Arguments; | |
40 import tango.util.log.Log : Log, Logger; | |
41 import tango.util.log.ConsoleAppender : ConsoleAppender; | |
42 | |
43 //version = SwitchAppender; | |
44 version (SwitchAppender) { // My own variation, currently just a test | |
45 import tango.util.log.SwitchingFileAppender : SwitchingFileAppender; | |
46 } else { | |
47 import tango.util.log.RollingFileAppender : RollingFileAppender; | |
48 } | |
49 | |
50 // Derelict imports | |
51 import derelict.opengl.gl; | |
52 import derelict.sdl.sdl; | |
53 //import derelict.sdl.image; Was to be used... for now isn't | |
54 import derelict.freetype.ft; | |
55 import derelict.util.exception; | |
56 | |
57 /** | |
58 * Static CTOR | |
59 * | |
60 * This should handle a minimal amount of functionality where useful. For instance, configuring the | |
61 * logger here and not in Init allows unittests to use the logger. | |
62 */ | |
63 static this() | |
64 { | |
65 Logger root = Log.getRootLogger(); | |
66 // Set the level here, but set it again once options have been loaded: | |
67 debug root.setLevel(root.Level.Trace); | |
68 else root.setLevel(root.Level.Info); | |
69 // Temporarily log to the console (until we've found paths and loaded options): | |
70 root.addAppender(new ConsoleAppender); | |
71 } | |
72 static ~this() | |
73 { | |
74 } | |
75 | |
76 /** | |
77 * Init class | |
78 * | |
79 * A scope class created at beginning of the program and destroyed at the end; thus the CTOR | |
80 * handles program initialisation and the DTOR handles program cleanup. | |
81 */ | |
82 scope class Init | |
83 { | |
84 //private bool failure = false; // set true on failure during init, so that clean | |
85 private static Logger logger; | |
86 static this() { | |
87 logger = Log.getLogger ("mde.setup.Init"); | |
88 } | |
89 | |
90 /** this() − initialisation | |
91 * | |
92 * Runs general initialisation code, in a threaded manner where this isn't difficult. | |
93 * If any init fails, cleanup is still handled by ~this(). | |
94 * | |
95 * Init order: 1. Pre-init (loads components needed by most init functions). 2. Dynamic library | |
96 * loading (load any dynamic libraries first, so that if loading fails nothing else need be | |
97 * done). 3. Init functions (threaded functions handling the rest of initialisation). | |
98 */ | |
99 /* In a single-threaded function this could be done with: | |
100 * scope(failure) cleanup; | |
101 * This won't work with a threaded init function since any threads completing succesfully will | |
102 * not clean-up, and a fixed list of clean-up functions cannot be used since clean-up functions | |
103 * must not run where the initialisation functions have failed. | |
104 * Hence a list of clean-up functions is built similarly to scope(failure) --- see addCleanupFct. | |
105 */ | |
106 this(char[][] cmdArgs) | |
107 { | |
108 debug logger.trace ("Init: starting"); | |
109 | |
110 //BEGIN Pre-init (stage init0) | |
111 //FIXME: warn on invalid arguments, including base-path on non-Windows | |
112 // But Arguments doesn't support this (in tango 0.99.6 and in r3563). | |
113 Arguments args; | |
114 try { | |
115 args = new Arguments(); | |
116 args.define("base-path").parameters(1); | |
117 args.define("data-path").parameters(1,-1); | |
118 args.define("conf-path").parameters(1,-1); | |
119 args.define("paths"); | |
120 args.define("q").aliases(["quick-exit"]); | |
121 args.define("help").aliases(["h"]); | |
122 args.parse(cmdArgs); | |
123 if (args.contains("help")) // lazy way to print help | |
124 throw new InitException ("Help requested"); // and stop | |
125 } catch (Exception e) { | |
126 printUsage(cmdArgs[0]); | |
127 throw new InitException ("Parsing arguments failed: "~e.msg); | |
128 } | |
129 | |
130 // Find/create paths: | |
131 try { | |
132 if (args.contains("data-path")) | |
133 paths.extraDataPath = args["data-path"]; | |
134 if (args.contains("conf-path")) | |
135 paths.extraConfPath = args["conf-path"]; | |
136 if (args.contains("base-path")) | |
137 paths.resolvePaths (args["base-path"]); | |
138 else | |
139 paths.resolvePaths(); | |
140 } catch (Exception e) { | |
141 throw new InitException ("Resolving paths failed: " ~ e.msg); | |
142 } | |
143 if (args.contains("paths")) { | |
144 paths.mdeDirectory.printPaths; | |
145 throw new InitException ("Paths requested"); // lazy way to stop | |
146 } | |
147 debug logger.trace ("Init: resolved paths successfully"); | |
148 | |
149 /* Load options now. Don't load in a thread since: | |
150 * Loading should be fast & most work is probably disk access | |
151 * It enables logging to be controlled by options | |
152 * It's a really good idea to let the options apply to all other loading */ | |
153 try { | |
154 Options.load(); | |
155 } catch (optionsLoadException e) { | |
156 throw new InitException ("Loading options failed: " ~ e.msg); | |
157 } | |
158 debug logger.trace ("Init: loaded options successfully"); | |
159 | |
160 // Set up the logger: | |
161 Logger root; | |
162 try { | |
163 enum LOG { | |
164 LEVEL = 0xF, // mask for log level | |
165 CONSOLE = 0x1000, // log to console? | |
166 ROLLFILE= 0x2000 // use Rolling/Switching File Appender? | |
167 } | |
168 | |
169 // Where logging is done to is determined at compile-time, currently just via static ifs. | |
170 root = Log.getRootLogger(); | |
171 root.clearAppenders; // we may no longer want to log to the console | |
172 | |
173 // Now re-set the logging level, using the value from the config file: | |
174 root.setLevel (cast(Log.Level) (miscOpts.logOptions & LOG.LEVEL), true); | |
175 | |
176 // Log to a file (first appender so root seperator messages don't show on console): | |
177 if (miscOpts.logOptions & LOG.ROLLFILE) { | |
178 version (SwitchAppender) { | |
179 root.addAppender (new SwitchingFileAppender (paths.logDir~"/log-.txt", 5)); | |
180 } else { | |
181 // Use 2 log files with a maximum size of 1 MB: | |
182 root.addAppender (new RollingFileAppender (paths.logDir~"/log-.txt", 2, 1024*1024)); | |
183 root.info (""); // some kind of separation between runs | |
184 root.info (""); | |
185 } | |
186 } else if (!(miscOpts.logOptions & LOG.CONSOLE)) { | |
187 // make sure at least one logger is enabled | |
188 Options.setInt ("misc", "logOptions", miscOpts.logOptions | LOG.CONSOLE); | |
189 } | |
190 if (miscOpts.logOptions & LOG.CONSOLE) { // Log to the console | |
191 root.addAppender(new ConsoleAppender); | |
192 } | |
193 logger.info ("Starting mde [no version] on " ~ TimeStamp.toString(WallClock.now)); | |
194 } catch (Exception e) { | |
195 // Presumably it was only adding a file appender which failed; set up a new console | |
196 // logger and if that fails let the exception kill the program. | |
197 root.clearAppenders; | |
198 root.addAppender (new ConsoleAppender); | |
199 logger.warn ("Exception while setting up the logger; logging to the console instead."); | |
200 } | |
201 | |
202 // a debugging option: | |
203 imde.run = !args.contains("q") && !miscOpts.exitImmediately; | |
204 debug logger.trace ("Init: applied pre-init options"); | |
205 | |
206 //BEGIN Load dynamic libraries | |
207 /* Can be done by init functions but much neater to do here. | |
208 * Also means that init functions aren't run if a library fails to load. */ | |
209 try { | |
210 DerelictSDL.load(); | |
211 // SDLImage was going to be used... for now it isn't because of gl texturing problems | |
212 //DerelictSDLImage.load(); | |
213 DerelictGL.load(); | |
214 DerelictFT.load(); | |
215 } catch (DerelictException de) { | |
216 logger.fatal ("Loading dynamic library failed:"); | |
217 logger.fatal (de.msg); | |
218 | |
219 throw new InitException ("Loading dynamic libraries failed (see above)."); | |
220 } | |
221 debug logger.trace ("Init: dynamic libraries loaded"); | |
222 //END Load dynamic libraries | |
223 //END Pre-init | |
224 | |
225 | |
226 //BEGIN Init (stages init2, init4) | |
227 /* Call init functions. | |
228 * | |
229 * Current method is to try using threads, and on failure assume no threads were actually | |
230 * created and run functions in a non-threaded manner. */ | |
231 | |
232 try { | |
233 if (runStageThreaded (init)) runStageForward (init); | |
234 } | |
235 catch (InitStageException) { // This init stage failed. | |
236 // FIXME: check DTOR still runs | |
237 throw new InitException ("An init function failed (see above message(s))"); | |
238 } | |
239 //END Init | |
240 | |
241 debug logger.trace ("Init: done"); | |
242 } | |
243 | |
244 /** DTOR - runs cleanup functions. */ | |
245 ~this() | |
246 { | |
247 debug logger.trace ("Cleanup: starting"); | |
248 | |
249 Options.save(); // save options... do so here for now | |
250 | |
251 // General cleanup: | |
252 try { | |
253 if (runStageThreaded (cleanup)) runStageReverse (cleanup); | |
254 } | |
255 catch (InitStageException) { | |
256 // Nothing else to do but report: | |
257 logger.error ("One or more cleanup functions failed!"); | |
258 } | |
259 | |
260 debug logger.trace ("Cleanup: done"); | |
261 } | |
262 | |
263 | |
264 //BEGIN runStage... | |
265 private static { | |
266 /* The following three functions, runStage*, each run all functions in a stage in some order, | |
267 * catching any exceptions thrown by the functions (although this isn't guaranteed for threads), | |
268 * and throw an InitStageException on initFailure. */ | |
269 | |
270 const LOG_IF_MSG = "Init function "; | |
271 const LOG_CF_MSG = "Cleanup function "; | |
272 const LOG_F_START = " - running"; | |
273 const LOG_F_END = " - completed"; | |
274 const LOG_F_BAD = " - failed"; | |
275 const LOG_F_FAIL = " - failed: "; | |
276 /* Runs all functions consecutively, first-to-last. | |
277 * If any function fails, halts immediately. */ | |
278 void runStageForward (InitStage s) { | |
279 foreach (func; s.funcs) { | |
280 if (initFailure) break; | |
281 try { | |
282 debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START); | |
283 func.func(); | |
284 debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); | |
285 } catch (Exception e) { | |
286 logger.fatal (LOG_IF_MSG ~ func.name ~ LOG_F_FAIL ~ | |
287 ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) ); | |
288 | |
289 setInitFailure(); | |
290 } | |
291 } | |
292 | |
293 if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. | |
294 } | |
295 /* Runs all functions consecutively, last-to-first. | |
296 * If any function fails, continue until all have been run. */ | |
297 void runStageReverse (InitStage s) { | |
298 foreach_reverse (func; s.funcs) { | |
299 try { | |
300 debug logger.trace (LOG_CF_MSG ~ func.name ~ LOG_F_START); | |
301 func.func(); | |
302 debug logger.trace (LOG_CF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); | |
303 } catch (Exception e) { | |
304 logger.fatal (LOG_CF_MSG ~ func.name ~ LOG_F_FAIL ~ | |
305 ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) ); | |
306 | |
307 setInitFailure(); | |
308 } | |
309 } | |
310 if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. | |
311 } | |
312 /* Tries running functions in a threaded way. Returns false if successful, true if not but | |
313 * functions should be run without threads. */ | |
314 bool runStageThreaded (InitStage s) { | |
315 if (!miscOpts.useThreads) return true; // Use unthreaded route instead | |
316 | |
317 ThreadGroup tg; | |
318 try { // creating/starting threads could fail | |
319 tg = new ThreadGroup; | |
320 foreach (func; s.funcs) { // Start all threads | |
321 debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START); | |
322 tg.create(func.func); | |
323 debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); | |
324 } | |
325 } catch (ThreadException e) { // Problem with threading; try without threads | |
326 logger.error ("Caught ThreadException while trying to create threads:"); | |
327 logger.error (e.msg); | |
328 logger.info ("Will disable threads and continue, assuming no threads were created."); | |
329 | |
330 Options.setBool("misc", "useThreads", false); // Disable threads entirely | |
331 return true; // Try again without threads | |
332 } | |
333 | |
334 /* Wait for all threads to complete. | |
335 * | |
336 * If something went wrong, we still need to do this before cleaning up. | |
337 */ | |
338 foreach (t; tg) { | |
339 try { | |
340 t.join (true); | |
341 } catch (Exception e) { | |
342 // Relying on catching exceptions thrown by other threads is a bad idea. | |
343 // Hence all threads should catch their own exceptions and return safely. | |
344 | |
345 logger.fatal ("Unhandled exception from Init function:"); | |
346 logger.fatal (e.msg); | |
347 | |
348 setInitFailure (); // abort (but join other threads first) | |
349 } | |
350 } | |
351 | |
352 if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. | |
353 return false; // Done successfully | |
354 } | |
355 //END runStage... | |
356 | |
357 void printUsage (char[] progName) { | |
358 Cout ("mde [no version]").newline; | |
359 Cout ("Usage:").newline; | |
360 Cout (progName ~ ` [options]`).newline; | |
361 version(Windows) | |
362 Cout ( | |
363 ` --base-path path Use path as the base (install) path (Windows only). It | |
364 should contain the "data" directory.`).newline; | |
365 Cout ( | |
366 ` --data-path path(s) Add path(s) as a potential location for data files. | |
367 First path argument becomes the preffered location to | |
368 load data files from. | |
369 --conf-path path(s) Add path(s) as a potential location for config files. | |
370 Configuration in the first path given take highest | |
371 priority. | |
372 --paths Print all paths found and exit. | |
373 --quick-exit, -q Exit immediately, without entering main loop. | |
374 --help, -h Print this message.`).newline; | |
375 } | |
376 } | |
377 | |
378 debug (mdeUnitTest) unittest { | |
379 /* Fake init and cleanup. Use unittest-specific init and cleanup InitStages to avoid | |
380 * messing other init/cleanup up. */ | |
381 static InitStage initUT, cleanupUT; | |
382 | |
383 static bool initialised = false; | |
384 static void cleanupFunc1 () { | |
385 initialised = false; | |
386 } | |
387 static void cleanupFunc2 () { | |
388 assert (initialised == true); | |
389 } | |
390 | |
391 static void initFunc () { | |
392 initialised = true; | |
393 cleanupUT.addFunc (&cleanupFunc1, "UT cleanup 1"); | |
394 cleanupUT.addFunc (&cleanupFunc2, "UT cleanup 2"); | |
395 } | |
396 | |
397 initUT.addFunc (&initFunc, "UT init"); | |
398 | |
399 runStageForward (initUT); | |
400 assert (initialised); | |
401 | |
402 runStageReverse (cleanupUT); | |
403 assert (!initialised); | |
404 | |
405 logger.info ("Unittest complete."); | |
406 } | |
407 } |