|
1 /* |
|
2 * Copyright (c) 2003-2005 Nokia Corporation and/or its subsidiary(-ies). |
|
3 * All rights reserved. |
|
4 * This component and the accompanying materials are made available |
|
5 * under the terms of "Eclipse Public License v1.0" |
|
6 * which accompanies this distribution, and is available |
|
7 * at the URL "http://www.eclipse.org/legal/epl-v10.html". |
|
8 * |
|
9 * Initial Contributors: |
|
10 * Nokia Corporation - initial contribution. |
|
11 * |
|
12 * Contributors: |
|
13 * |
|
14 * Description: |
|
15 * |
|
16 */ |
|
17 |
|
18 |
|
19 #include "s_lib.h" |
|
20 #include "instreg.h" |
|
21 |
|
22 /* Exit codes */ |
|
23 #define EXITCODE_SUCCESS 0 /* successful completion */ |
|
24 #define EXITCODE_HELP 1 /* help printed, nothing done */ |
|
25 #define EXITCODE_CMDLINE 2 /* command line parse error */ |
|
26 #define EXITCODE_IOERR 3 /* cannot write installation registry */ |
|
27 #define EXITCODE_NOMEM 4 /* out of memory (very unlikely) */ |
|
28 |
|
29 /* Data structures */ |
|
30 struct _MemContext { |
|
31 const MemHook * next; |
|
32 }; |
|
33 |
|
34 typedef struct _AppContext { |
|
35 Bool unregister; /* true if we are unregistering the product */ |
|
36 StrBuf sb; /* temporary string buffer */ |
|
37 DOMNode* root; /* root node of the DOM tree */ |
|
38 Str name; /* product name */ |
|
39 Str ver; /* product version, NULL if unknown */ |
|
40 Str loc; /* product location */ |
|
41 Str exe; /* product executable */ |
|
42 Str type; /* product type */ |
|
43 Str nameEsc; /* XML-escaped product name */ |
|
44 Str typeEsc; /* XML-escaped product type */ |
|
45 Str verEsc; /* XML-escaped version */ |
|
46 Str exeEsc; /* XML-escaped path */ |
|
47 Str locEsc; /* XML-escaped location */ |
|
48 } AppContext; |
|
49 |
|
50 /* Tags in the installation registry file */ |
|
51 #define REGISTRY_TAG "registry" |
|
52 #define PRODUCT_TAG "product" |
|
53 #define NAME_TAG "name" |
|
54 #define VERSION_TAG "version" |
|
55 #define EXECUTABLE_TAG "executable" |
|
56 #define LOCATION_TAG "location" |
|
57 #define TYPE_ATTR "type" |
|
58 #define EMULATOR_TYPE "emulator" |
|
59 |
|
60 #define ROOT_TAG REGISTRY_TAG |
|
61 |
|
62 /* |
|
63 * Some default values. Note that we are making an assumption that the |
|
64 * default values don't need to be XML-escaped. |
|
65 */ |
|
66 #define DEFAULT_TYPE EMULATOR_TYPE |
|
67 #define DEFAULT_VERSION "1.0" |
|
68 |
|
69 /* Desctiption of the command line parameters */ |
|
70 #define CMDLINE_PARAM "PRODUCT" |
|
71 |
|
72 /* Modifiable global data */ |
|
73 static Char* pname = "instreg"; |
|
74 static Char regfile[] = "C:\\Nokia\\Registry\\installationRegistry.xml"; |
|
75 |
|
76 /* Constants */ |
|
77 static const Str commonHeader = "\ |
|
78 <?xml version=\"1.0\" standalone=\"yes\"?>\n\ |
|
79 \n\ |
|
80 <!DOCTYPE "REGISTRY_TAG" [\n\ |
|
81 <!ELEMENT "REGISTRY_TAG" ("PRODUCT_TAG")*>\n\ |
|
82 <!ELEMENT "PRODUCT_TAG" ("NAME_TAG", "VERSION_TAG", "EXECUTABLE_TAG", "\ |
|
83 LOCATION_TAG")>\n\ |
|
84 <!ELEMENT "NAME_TAG" (#PCDATA)>\n\ |
|
85 <!ELEMENT "VERSION_TAG" (#PCDATA)>\n\ |
|
86 <!ELEMENT "EXECUTABLE_TAG" (#PCDATA)>\n\ |
|
87 <!ELEMENT "LOCATION_TAG" (#PCDATA)>\n\ |
|
88 <!ATTLIST "PRODUCT_TAG" "TYPE_ATTR" CDATA #IMPLIED>\n\ |
|
89 ]>\n\ |
|
90 \n\ |
|
91 "; |
|
92 |
|
93 /* This template is used when we are creating a new file */ |
|
94 static const Str newTemplate = "%s\ |
|
95 <"REGISTRY_TAG">\n\ |
|
96 <"PRODUCT_TAG" "TYPE_ATTR"=\"%s\">\n\ |
|
97 <"NAME_TAG">\n\ |
|
98 %s\n\ |
|
99 </"NAME_TAG">\n\ |
|
100 <"VERSION_TAG">\n\ |
|
101 %s\n\ |
|
102 </"VERSION_TAG">\n\ |
|
103 <"EXECUTABLE_TAG">\n\ |
|
104 %s\n\ |
|
105 </"EXECUTABLE_TAG">\n\ |
|
106 <"LOCATION_TAG">\n\ |
|
107 %s\n\ |
|
108 </"LOCATION_TAG">\n\ |
|
109 </"PRODUCT_TAG">\n\ |
|
110 </"REGISTRY_TAG">\n\ |
|
111 "; |
|
112 |
|
113 /* This template is used when we are updating the existing entry */ |
|
114 static const Str updateTemplate = "\ |
|
115 <"PRODUCT_TAG" "TYPE_ATTR"=\"%s\">\n\ |
|
116 <"NAME_TAG">\n\ |
|
117 %s\n\ |
|
118 </"NAME_TAG">\n\ |
|
119 <"VERSION_TAG">\n\ |
|
120 %s\n\ |
|
121 </"VERSION_TAG">\n\ |
|
122 <"EXECUTABLE_TAG">\n\ |
|
123 %s\n\ |
|
124 </"EXECUTABLE_TAG">\n\ |
|
125 <"LOCATION_TAG">\n\ |
|
126 %s\n\ |
|
127 </"LOCATION_TAG">\n\ |
|
128 </"PRODUCT_TAG">\ |
|
129 "; |
|
130 |
|
131 /* Template for appending a new entry to the existing file */ |
|
132 static const Str appendTemplate = "\ |
|
133 <"PRODUCT_TAG" "TYPE_ATTR"=\"%s\">\n\ |
|
134 <"NAME_TAG">\n\ |
|
135 %s\n\ |
|
136 </"NAME_TAG">\n\ |
|
137 <"VERSION_TAG">\n\ |
|
138 %s\n\ |
|
139 </"VERSION_TAG">\n\ |
|
140 <"EXECUTABLE_TAG">\n\ |
|
141 %s\n\ |
|
142 </"EXECUTABLE_TAG">\n\ |
|
143 <"LOCATION_TAG">\n\ |
|
144 %s\n\ |
|
145 </"LOCATION_TAG">\n\ |
|
146 </"PRODUCT_TAG">\n\ |
|
147 "; |
|
148 |
|
149 /** |
|
150 * Memory hooks. If memory allocation fails, we exit the program with |
|
151 * EXITCODE_NOMEM status. Therefore, we don't need for check for memory |
|
152 * allocation failures in the code. |
|
153 */ |
|
154 static Bool APP_MemInit(MemContext* ctx, const MemHook* next) |
|
155 { |
|
156 ctx->next = next; |
|
157 return True; |
|
158 } |
|
159 |
|
160 static void* APP_MemAlloc(MemContext* ctx, size_t size) |
|
161 { |
|
162 void* ptr = MEM_Alloc2(ctx->next, size); |
|
163 if (!ptr) { |
|
164 TRACE_Error("Out of memory!\n"); |
|
165 exit(EXITCODE_NOMEM); |
|
166 } |
|
167 return ptr; |
|
168 } |
|
169 |
|
170 static Bool APP_PrintTag(File* out, Str tag, const XMLAttr* a, StrBuf* sb) |
|
171 { |
|
172 if (FILE_Puts(out, TEXT("<")) && FILE_Puts(out, tag)) { |
|
173 int i; |
|
174 int n = XML_AttrCount(a); |
|
175 for (i=0; i<n; i++) { |
|
176 Str name = XML_AttrNameAt(a, i); |
|
177 Str value = XML_AttrValueAt(a, i); |
|
178 Str esc = XML_Escape(NULL, value); |
|
179 if (!esc) esc = XML_Escape(sb, value); |
|
180 if (FILE_Printf(out, TEXT(" %s=\"%s\""), name, esc) < 0) { |
|
181 return False; |
|
182 } |
|
183 } |
|
184 return (FILE_Puts(out, TEXT(">")) > 0); |
|
185 } |
|
186 return False; |
|
187 } |
|
188 |
|
189 /** |
|
190 * Prints the DOM node to the specified stream. |
|
191 */ |
|
192 static Bool APP_PrintDOM(File* out, DOMNode* node, StrBuf* sb) |
|
193 { |
|
194 if (APP_PrintTag(out, DOM_TagName(node),DOM_Attr(node), sb)) { |
|
195 if (DOM_ChunkCount(node) == 0) { |
|
196 Str charData = DOM_CharData(node); |
|
197 if (charData && FILE_Printf(out, TEXT("%s"), charData) < 0) { |
|
198 return False; |
|
199 } |
|
200 } else { |
|
201 DOMChunk* chunk = DOM_FirstChunk(node); |
|
202 while (chunk) { |
|
203 DOMNode* child = DOM_ChunkNode(chunk); |
|
204 Str text = DOM_ChunkText(chunk); |
|
205 ASSERT(text || child); |
|
206 if ((text && !FILE_Puts(out, text)) || |
|
207 (child && !APP_PrintDOM(out, child, sb))) { |
|
208 return False; |
|
209 } |
|
210 chunk=DOM_NextChunk(chunk); |
|
211 } |
|
212 } |
|
213 return (FILE_Printf(out, TEXT("</%s>"),DOM_TagName(node)) > 0); |
|
214 } |
|
215 return False; |
|
216 } |
|
217 |
|
218 /** |
|
219 * Matches the child tag of the product node against the expected value |
|
220 */ |
|
221 static Bool APP_MatchInfo(DOMNode* p, Str tag, Str expect, StrBuf* sb) |
|
222 { |
|
223 if (expect) { |
|
224 DOMNode* node = DOM_FindFirst(p, tag); |
|
225 if (node) { |
|
226 Str data = DOM_CharData(node); |
|
227 if (data && StrStr(data, expect)) { |
|
228 STRBUF_Copy(sb, data); |
|
229 STRBUF_Trim(sb); |
|
230 return STRBUF_EqualsTo(sb, expect); |
|
231 } |
|
232 } |
|
233 /* no match */ |
|
234 return False; |
|
235 } else { |
|
236 /* nothing is expected, anything will do */ |
|
237 return True; |
|
238 } |
|
239 } |
|
240 |
|
241 /** |
|
242 * Checks whether the node matches the product criteria specified on |
|
243 * the command line. This serves dual purpose. When registering the |
|
244 * product, this is how we find the existing entry to update. The |
|
245 * first entry that matches the criteria will be updated, all others |
|
246 * ignored. When unregistering the product, all matching entries are |
|
247 * removed. |
|
248 */ |
|
249 static Bool APP_MatchProduct(DOMNode* product, AppContext* app) |
|
250 { |
|
251 Bool match = False; |
|
252 if (product && StrCmp(DOM_TagName(product),PRODUCT_TAG) == 0) { |
|
253 Str type = XML_AttrValue(DOM_Attr(product),TYPE_ATTR); |
|
254 if (!app->type || (type && StrCmp(type,app->type) == 0)) { |
|
255 /* compare product information */ |
|
256 if (APP_MatchInfo(product, NAME_TAG, app->name, &app->sb) && |
|
257 APP_MatchInfo(product, VERSION_TAG, app->ver, &app->sb) && |
|
258 APP_MatchInfo(product, LOCATION_TAG, app->loc, &app->sb) && |
|
259 APP_MatchInfo(product, EXECUTABLE_TAG, app->exe, &app->sb)) { |
|
260 match = True; |
|
261 } |
|
262 } |
|
263 } |
|
264 return match; |
|
265 } |
|
266 |
|
267 /** |
|
268 * FILE_Save callback. If invoked from FILE_Save, this callback is actually |
|
269 * writing to a temporary file. The temporary file then gets renamed into the |
|
270 * actual output file. This guarantees that we either successfully update the |
|
271 * file, or don't change it at all. Note that we also invoke this callback |
|
272 * directly, to write to stdout. |
|
273 */ |
|
274 static Bool APP_FileSaveCB(File* out, Str fname, void * ctx) |
|
275 { |
|
276 AppContext* app = ctx; |
|
277 Str tag = DOM_TagName(app->root); |
|
278 if (FILE_Puts(out, commonHeader) && |
|
279 APP_PrintTag(out, tag, DOM_Attr(app->root), &app->sb)) { |
|
280 Bool updated = False; |
|
281 DOMChunk* chunk = DOM_FirstChunk(app->root); |
|
282 while (chunk) { |
|
283 DOMNode* child = DOM_ChunkNode(chunk); |
|
284 Str text = DOM_ChunkText(chunk); |
|
285 if (child) { |
|
286 if (APP_MatchProduct(child, app)) { |
|
287 |
|
288 /* |
|
289 * If unregistering, skip this entry. Also, when |
|
290 * we find more that one matching entry when we are |
|
291 * registering the product, we update the first one |
|
292 * and remove the others. |
|
293 */ |
|
294 if (!app->unregister && !updated) { |
|
295 /* update the existing entry */ |
|
296 if (!FILE_Puts(out, text) || |
|
297 FILE_Printf(out, updateTemplate, |
|
298 app->typeEsc, app->nameEsc, |
|
299 app->verEsc, app->exeEsc, |
|
300 app->locEsc) > 0) { |
|
301 updated = True; |
|
302 } else { |
|
303 return False; |
|
304 } |
|
305 } |
|
306 } else if (!FILE_Puts(out, text) || |
|
307 !APP_PrintDOM(out, child, &app->sb)) { |
|
308 return False; |
|
309 } |
|
310 } else if (text) { |
|
311 if (!FILE_Puts(out, text)) { |
|
312 return False; |
|
313 } |
|
314 } |
|
315 chunk = DOM_NextChunk(chunk); |
|
316 } |
|
317 if (!updated) { |
|
318 if (DOM_ChunkCount(app->root) == 0) { |
|
319 Str charData = DOM_CharData(app->root); |
|
320 if (charData && FILE_Printf(out, TEXT("%s"), charData) < 0) { |
|
321 return False; |
|
322 } |
|
323 } |
|
324 if (!app->unregister) { |
|
325 /* append a new entry */ |
|
326 if (FILE_Printf(out,appendTemplate, app->typeEsc, |
|
327 app->nameEsc, app->verEsc, app->exeEsc, |
|
328 app->locEsc) < 0) { |
|
329 return False; |
|
330 } |
|
331 } |
|
332 } |
|
333 return (FILE_Printf(out, TEXT("</%s>\n"),tag) > 0); |
|
334 } |
|
335 |
|
336 /* some kind of I/O error */ |
|
337 return False; |
|
338 } |
|
339 |
|
340 /** |
|
341 * The program entry point. |
|
342 */ |
|
343 int main(int argc, char* argv[]) |
|
344 { |
|
345 AppContext app; |
|
346 Bool toStdout = False; |
|
347 Bool help = False; |
|
348 Vector params; |
|
349 CmdLine* c; |
|
350 Str prog; |
|
351 int status; |
|
352 Char sysDirBuf[MAX_PATH +1]; |
|
353 |
|
354 MemProc mp; |
|
355 MemContext mc; |
|
356 |
|
357 /* install memory hook so that we don't have to check for NULL */ |
|
358 MEM_InitModule(); |
|
359 memset(&mc, 0, sizeof(mc)); |
|
360 memset(&mp, 0, sizeof(mp)); |
|
361 mp.memInit = APP_MemInit; |
|
362 mp.memAlloc = APP_MemAlloc; |
|
363 MEM_Hook(&mp, &mc); |
|
364 RANDOM_InitModule(); |
|
365 |
|
366 /* figure out the program name */ |
|
367 prog = FILE_FilePart(argv[0]); |
|
368 #ifdef _WIN32 |
|
369 if (STRING_EndsWith(prog, ".exe")) { |
|
370 int n = StrLen(prog)-4; |
|
371 pname = MEM_NewArray(Char, n+1); |
|
372 StrnCpy(pname, prog, n); |
|
373 pname[n] = 0; |
|
374 } else |
|
375 #endif /* _WIN32 */ |
|
376 pname = STRING_Dup(prog); |
|
377 |
|
378 /* |
|
379 * determine the drive where Windows is installed. That's the drive |
|
380 * where installationRegistry.xml file is located. |
|
381 */ |
|
382 #ifdef _WIN32 |
|
383 if (GetSystemDirectory(sysDirBuf, COUNT(sysDirBuf))) { |
|
384 TRACE1("system drive is %c:\n",sysDirBuf[0]); |
|
385 regfile[0] = sysDirBuf[0]; |
|
386 } |
|
387 #endif /* _WIN32 */ |
|
388 |
|
389 memset(&app, 0, sizeof(app)); |
|
390 c = CMDLINE_Create(pname); |
|
391 |
|
392 #ifdef _CONSOLE |
|
393 CMDLINE_AddTrueOpt(c,'h',"help","print this help and exit",&help); |
|
394 CMDLINE_AddTrueOpt(c,'c',"stdout","write updated installation registry " |
|
395 "to stdout",&toStdout); |
|
396 #endif /* _CONSOLE */ |
|
397 |
|
398 CMDLINE_SetParamName(CMDLINE_AddStrOpt(c,'e',"executable", |
|
399 "path to the executable (required to register)",&app.exe), "FILE"); |
|
400 CMDLINE_SetParamName(CMDLINE_AddStrOpt(c,'v',"version", |
|
401 "product version (default is "DEFAULT_VERSION")",&app.ver),"VERSION"); |
|
402 CMDLINE_SetParamName(CMDLINE_AddStrOpt(c,'l',"location", |
|
403 "product location (default is the executable folder)",&app.loc),"DIR"); |
|
404 CMDLINE_SetParamName(CMDLINE_AddStrOpt(c,'t',"type", |
|
405 "product type (default is "DEFAULT_TYPE")",&app.type),"TYPE"); |
|
406 CMDLINE_AddTrueOpt(c,'u',"unregister","unregister the product " |
|
407 "(default is to register)",&app.unregister); |
|
408 |
|
409 VECTOR_Init(¶ms, 0, vectorEqualsString, NULL); |
|
410 if (!CMDLINE_Parse(c,argv+1,argc-1,PARSE_NO_DUP,¶ms) || |
|
411 VECTOR_IsEmpty(¶ms) || help) { |
|
412 TRACE_Output(PRODUCT_NAME " Version %d.%d.%d\nCopyright (C) " |
|
413 PRODUCT_COPYRIGHT ". All rights reserved.\n\n", |
|
414 PRODUCT_VERSION_MAJOR, PRODUCT_VERSION_MINOR, |
|
415 PRODUCT_VERSION_MICRO); |
|
416 CMDLINE_Usage(c, CMDLINE_PARAM, 0); |
|
417 status = (help ? EXITCODE_HELP : EXITCODE_CMDLINE); |
|
418 } else if (VECTOR_Size(¶ms) > 1) { |
|
419 TRACE_Error("%s: unexpected command line parameters\n", pname); |
|
420 CMDLINE_Usage(c, CMDLINE_PARAM, 0); |
|
421 status = EXITCODE_CMDLINE; |
|
422 } else if (!app.exe && !app.unregister) { |
|
423 TRACE_Error("%s: product executable is required\n", pname); |
|
424 CMDLINE_Usage(c, CMDLINE_PARAM, 0); |
|
425 status = EXITCODE_CMDLINE; |
|
426 } else { |
|
427 |
|
428 StrBuf typeBuf; |
|
429 StrBuf nameBuf; |
|
430 StrBuf verBuf; |
|
431 StrBuf exeBuf; |
|
432 StrBuf locBuf; |
|
433 Char* dir = NULL; |
|
434 |
|
435 if (app.exe && !app.loc) { |
|
436 /* default location */ |
|
437 if (FILE_FilePart(app.exe) == app.exe) { |
|
438 int n = GetCurrentDirectory(0,NULL)+1; |
|
439 dir = MEM_NewArray(Char,n); |
|
440 dir[GetCurrentDirectory(n,dir)] = 0; |
|
441 } else { |
|
442 dir = FILE_DirName(app.exe, 0); |
|
443 } |
|
444 if (STRING_EndsWith(dir, FILE_SEPARATOR)) { |
|
445 dir[StrLen(dir)-1] = 0; |
|
446 } |
|
447 app.loc = dir; |
|
448 } |
|
449 |
|
450 status = EXITCODE_SUCCESS; |
|
451 |
|
452 /* XML escape the strings */ |
|
453 STRBUF_Init(&typeBuf); |
|
454 STRBUF_Init(&nameBuf); |
|
455 STRBUF_Init(&verBuf); |
|
456 STRBUF_Init(&exeBuf); |
|
457 STRBUF_Init(&locBuf); |
|
458 STRBUF_Init(&app.sb); |
|
459 app.name = VECTOR_Get(¶ms, 0); |
|
460 app.nameEsc = XML_Escape(&nameBuf, app.name); |
|
461 if (app.exe) app.exeEsc = XML_Escape(&exeBuf, app.exe); |
|
462 if (app.loc) app.locEsc = XML_Escape(&locBuf, app.loc); |
|
463 |
|
464 if (app.type) { |
|
465 app.typeEsc = XML_Escape(&typeBuf, app.type); |
|
466 } else { |
|
467 /* we know there's nothing to escape here */ |
|
468 app.typeEsc = DEFAULT_TYPE; |
|
469 } |
|
470 |
|
471 if (app.ver) { |
|
472 app.verEsc = XML_Escape(&verBuf, app.ver); |
|
473 } else { |
|
474 /* we know there's nothing to escape here */ |
|
475 app.verEsc = DEFAULT_VERSION; |
|
476 } |
|
477 |
|
478 app.root = DOM_Load(regfile); |
|
479 if (app.root && StrCmp(DOM_TagName(app.root),ROOT_TAG) == 0) { |
|
480 if (toStdout) { |
|
481 File* out = FILE_AttachToFile(stdout,"stdout"); |
|
482 APP_FileSaveCB(out, FILE_Name(out), &app); |
|
483 FILE_Close(out); |
|
484 } else { |
|
485 if (!FILE_Save(regfile, APP_FileSaveCB, &app, NULL)) { |
|
486 status = EXITCODE_IOERR; |
|
487 } |
|
488 } |
|
489 } else if (!app.unregister) { |
|
490 |
|
491 /* |
|
492 * either the file does not exist, or else it exists but |
|
493 * contains some garbage (root tag does not match the |
|
494 * expectation). Create a new file, unless the user has |
|
495 * given us -u (unregister) option in which case we have |
|
496 * nothing to do. |
|
497 */ |
|
498 if (toStdout) { |
|
499 printf(newTemplate, commonHeader, app.typeEsc, app.nameEsc, |
|
500 app.verEsc, app.exeEsc, app.locEsc); |
|
501 } else { |
|
502 |
|
503 /* create a new file */ |
|
504 File* f = FILE_Open(regfile, WRITE_TEXT_MODE, NULL); |
|
505 if (!f) { |
|
506 /* perhaps, the directory does not exist? */ |
|
507 Char* regdir = FILE_DirName(regfile, 0); |
|
508 FILE_MkDir(regdir); |
|
509 MEM_Free(regdir); |
|
510 } |
|
511 |
|
512 /* try again */ |
|
513 f = FILE_Open(regfile, WRITE_TEXT_MODE, NULL); |
|
514 if (f) { |
|
515 if (FILE_Printf(f, newTemplate, commonHeader, |
|
516 app.typeEsc, app.nameEsc, |
|
517 app.verEsc, app.exeEsc, |
|
518 app.locEsc) < 0) { |
|
519 status = EXITCODE_IOERR; |
|
520 } |
|
521 FILE_Close(f); |
|
522 } else { |
|
523 status = EXITCODE_IOERR; |
|
524 } |
|
525 } |
|
526 } |
|
527 |
|
528 STRBUF_Destroy(&app.sb); |
|
529 DOM_Delete(app.root); |
|
530 STRBUF_Destroy(&typeBuf); |
|
531 STRBUF_Destroy(&nameBuf); |
|
532 STRBUF_Destroy(&verBuf); |
|
533 STRBUF_Destroy(&exeBuf); |
|
534 STRBUF_Destroy(&locBuf); |
|
535 MEM_Free(dir); |
|
536 } |
|
537 |
|
538 /* Cleanup */ |
|
539 MEM_Free(pname); |
|
540 VECTOR_Destroy(¶ms); |
|
541 CMDLINE_Delete(c); |
|
542 RANDOM_Shutdown(); |
|
543 MEM_Shutdown(); |
|
544 return status; |
|
545 } |