1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 *
6 * Copyright 1997 - July 2008 CWI, August 2008 - 2019 MonetDB B.V.
7 */
8
9#include "monetdb_config.h"
10#include <string.h> /* strerror */
11#include <locale.h>
12#include "monet_options.h"
13#include "mal.h"
14#include "mal_session.h"
15#include "mal_import.h"
16#include "mal_client.h"
17#include "mal_function.h"
18#include "monet_version.h"
19#include "mal_authorize.h"
20#include "msabaoth.h"
21#include "mutils.h"
22
23#ifdef HAVE_LIBGEN_H
24#include <libgen.h>
25#endif
26
27#ifndef HAVE_GETOPT_LONG
28# include "monet_getopt.h"
29#else
30# ifdef HAVE_GETOPT_H
31# include "getopt.h"
32# endif
33#endif
34
35#ifdef _MSC_VER
36#include <Psapi.h> /* for GetModuleFileName */
37#include <crtdbg.h> /* for _CRT_ERROR, _CRT_ASSERT */
38#endif
39
40#ifdef _CRTDBG_MAP_ALLOC
41/* Windows only:
42 our definition of new and delete clashes with the one if
43 _CRTDBG_MAP_ALLOC is defined.
44 */
45#undef _CRTDBG_MAP_ALLOC
46#endif
47
48#if defined(_MSC_VER) && _MSC_VER >= 1400
49#define getcwd _getcwd
50#endif
51
52/* NEEDED? */
53#if defined(_MSC_VER) && defined(__cplusplus)
54#include <eh.h>
55void
56mserver_abort()
57{
58 fprintf(stderr, "\n! mserver_abort() was called by terminate(). !\n");
59 fflush(stderr);
60 exit(0);
61}
62#endif
63
64#ifdef _MSC_VER
65static void
66mserver_invalid_parameter_handler(
67 const wchar_t *expression,
68 const wchar_t *function,
69 const wchar_t *file,
70 unsigned int line,
71 uintptr_t reserved)
72{
73 (void) expression;
74 (void) function;
75 (void) file;
76 (void) line;
77 (void) reserved;
78 /* the essential bit of this function is that it returns:
79 * we don't want the server to quit when a function is called
80 * with an invalid parameter */
81}
82#endif
83
84static _Noreturn void usage(char *prog, int xit);
85
86static void
87usage(char *prog, int xit)
88{
89 fprintf(stderr, "Usage: %s [options]\n", prog);
90 fprintf(stderr, " --dbpath=<directory> Specify database location\n");
91 fprintf(stderr, " --dbextra=<directory> Directory for transient BATs\n");
92 fprintf(stderr, " --in-memory Run database in-memory only\n");
93 fprintf(stderr, " --config=<config_file> Use config_file to read options from\n");
94 fprintf(stderr, " --single-user Allow only one user at a time\n");
95 fprintf(stderr, " --readonly Safeguard database\n");
96 fprintf(stderr, " --set <option>=<value> Set configuration option\n");
97 fprintf(stderr, " --help Print this list of options\n");
98 fprintf(stderr, " --version Print version and compile time info\n");
99 fprintf(stderr, " --verbose[=value] Set or increase verbosity level\n");
100
101 fprintf(stderr, "The debug, testing & trace options:\n");
102 fprintf(stderr, " --threads\n");
103 fprintf(stderr, " --memory\n");
104 fprintf(stderr, " --io\n");
105 fprintf(stderr, " --heaps\n");
106 fprintf(stderr, " --properties\n");
107 fprintf(stderr, " --transactions\n");
108 fprintf(stderr, " --modules\n");
109 fprintf(stderr, " --algorithms\n");
110 fprintf(stderr, " --performance\n");
111 fprintf(stderr, " --optimizers\n");
112 fprintf(stderr, " --forcemito\n");
113 fprintf(stderr, " --debug=<bitmask>\n");
114
115 exit(xit);
116}
117
118/*
119 * Collect some global system properties to relate performance results later
120 */
121static void
122monet_hello(void)
123{
124 dbl sz_mem_h;
125 char *qc = " kMGTPE";
126 int qi = 0;
127
128 printf("# MonetDB 5 server v%s", GDKversion());
129 {
130#ifdef MONETDB_RELEASE
131 printf(" (%s)", MONETDB_RELEASE);
132#else
133 const char *rev = mercurial_revision();
134 if (strcmp(rev, "Unknown") != 0)
135 printf(" (hg id: %s)", rev);
136#endif
137 }
138#ifndef MONETDB_RELEASE
139 printf("\n# This is an unreleased version");
140#endif
141 printf("\n# Serving database '%s', using %d thread%s\n",
142 GDKgetenv("gdk_dbname"),
143 GDKnr_threads, (GDKnr_threads != 1) ? "s" : "");
144 printf("# Compiled for %s/%zubit%s\n",
145 HOST, sizeof(ptr) * 8,
146#ifdef HAVE_HGE
147 " with 128bit integers"
148#else
149 ""
150#endif
151 );
152 sz_mem_h = (dbl) (MT_npages() * MT_pagesize());
153 while (sz_mem_h >= 1000.0 && qi < 6) {
154 sz_mem_h /= 1024.0;
155 qi++;
156 }
157 printf("# Found %.3f %ciB available main-memory",
158 sz_mem_h, qc[qi]);
159 sz_mem_h = (dbl) GDK_mem_maxsize;
160 qi = 0;
161 while (sz_mem_h >= 1000.0 && qi < 6) {
162 sz_mem_h /= 1024.0;
163 qi++;
164 }
165 printf(" of which we use %.3f %ciB\n",
166 sz_mem_h, qc[qi]);
167#ifdef MONET_GLOBAL_DEBUG
168 printf("# Database path:%s\n", GDKgetenv("gdk_dbpath"));
169 printf("# Module path:%s\n", GDKgetenv("monet_mod_path"));
170#endif
171 printf("# Copyright (c) 1993 - July 2008 CWI.\n");
172 printf("# Copyright (c) August 2008 - 2019 MonetDB B.V., all rights reserved\n");
173 printf("# Visit https://www.monetdb.org/ for further information\n");
174
175 // The properties shipped through the performance profiler
176 (void) snprintf(monet_characteristics, sizeof(monet_characteristics),
177 "{\n"
178 "\"version\":\"%s\",\n"
179 "\"release\":\"%s\",\n"
180 "\"host\":\"%s\",\n"
181 "\"threads\":\"%d\",\n"
182 "\"memory\":\"%.3f %cB\",\n"
183 "\"oid\":\"%zu\",\n"
184 "\"packages\":["
185#ifdef HAVE_HGE
186 "\"huge\""
187#endif
188 "]\n}",
189 GDKversion(),
190#ifdef MONETDB_RELEASE
191 MONETDB_RELEASE,
192#else
193 "unreleased",
194#endif
195 HOST, GDKnr_threads,
196 sz_mem_h, qc[qi], sizeof(oid) * 8);
197}
198
199static str
200absolute_path(str s)
201{
202 if (!MT_path_absolute(s)) {
203 str ret = (str) GDKmalloc(strlen(s) + strlen(monet_cwd) + 2);
204
205 if (ret)
206 sprintf(ret, "%s%c%s", monet_cwd, DIR_SEP, s);
207 return ret;
208 }
209 return GDKstrdup(s);
210}
211
212#define BSIZE 8192
213
214static int
215monet_init(opt *set, int setlen)
216{
217 /* determine Monet's kernel settings */
218 if (GDKinit(set, setlen) != GDK_SUCCEED)
219 return 0;
220
221#ifdef HAVE_SETSID
222 setsid();
223#endif
224 monet_hello();
225 return 1;
226}
227
228static void emergencyBreakpoint(void)
229{
230 /* just a handle to break after system initialization for GDB */
231}
232
233static volatile sig_atomic_t interrupted = 0;
234
235#ifdef _MSC_VER
236static BOOL WINAPI
237winhandler(DWORD type)
238{
239 (void) type;
240 interrupted = 1;
241 return TRUE;
242}
243#else
244static void
245handler(int sig)
246{
247 (void) sig;
248 interrupted = 1;
249}
250#endif
251
252int
253main(int argc, char **av)
254{
255 char *prog = *av;
256 opt *set = NULL;
257 int grpdebug = 0, debug = 0, setlen = 0;
258 str err = MAL_SUCCEED;
259 char prmodpath[FILENAME_MAX];
260 const char *modpath = NULL;
261 char *binpath = NULL;
262 char *dbpath = NULL;
263 char *dbextra = NULL;
264 int verbosity = 0;
265 bool inmemory = false;
266 static struct option long_options[] = {
267 { "config", required_argument, NULL, 'c' },
268 { "dbpath", required_argument, NULL, 0 },
269 { "dbextra", required_argument, NULL, 0 },
270 { "debug", optional_argument, NULL, 'd' },
271 { "help", no_argument, NULL, '?' },
272 { "version", no_argument, NULL, 0 },
273 { "verbose", optional_argument, NULL, 'v' },
274 { "readonly", no_argument, NULL, 'r' },
275 { "single-user", no_argument, NULL, 0 },
276 { "set", required_argument, NULL, 's' },
277 { "threads", no_argument, NULL, 0 },
278 { "memory", no_argument, NULL, 0 },
279 { "properties", no_argument, NULL, 0 },
280 { "io", no_argument, NULL, 0 },
281 { "transactions", no_argument, NULL, 0 },
282 { "modules", no_argument, NULL, 0 },
283 { "algorithms", no_argument, NULL, 0 },
284 { "optimizers", no_argument, NULL, 0 },
285 { "performance", no_argument, NULL, 0 },
286 { "forcemito", no_argument, NULL, 0 },
287 { "heaps", no_argument, NULL, 0 },
288 { "in-memory", no_argument, NULL, 0 },
289 { NULL, 0, NULL, 0 }
290 };
291
292#if defined(_MSC_VER) && defined(__cplusplus)
293 set_terminate(mserver_abort);
294#endif
295#ifdef _MSC_VER
296 _CrtSetReportMode(_CRT_ERROR, 0);
297 _CrtSetReportMode(_CRT_ASSERT, 0);
298 _set_invalid_parameter_handler(mserver_invalid_parameter_handler);
299#ifdef _TWO_DIGIT_EXPONENT
300 _set_output_format(_TWO_DIGIT_EXPONENT);
301#endif
302#endif
303 if (setlocale(LC_CTYPE, "") == NULL) {
304 fprintf(stderr, "cannot set locale\n");
305 exit(1);
306 }
307
308 if (getcwd(monet_cwd, FILENAME_MAX - 1) == NULL) {
309 perror("pwd");
310 fprintf(stderr,"monet_init: could not determine current directory\n");
311 exit(-1);
312 }
313
314 /* retrieve binpath early (before monet_init) because some
315 * implementations require the working directory when the binary was
316 * called */
317 binpath = get_bin_path();
318
319 if (!(setlen = mo_builtin_settings(&set)))
320 usage(prog, -1);
321
322 for (;;) {
323 int option_index = 0;
324
325 int c = getopt_long(argc, av, "c:d::rs:t::v::?",
326 long_options, &option_index);
327
328 if (c == -1)
329 break;
330
331 switch (c) {
332 case 0:
333 if (strcmp(long_options[option_index].name, "in-memory") == 0) {
334 inmemory = true;
335 break;
336 }
337 if (strcmp(long_options[option_index].name, "dbpath") == 0) {
338 size_t optarglen = strlen(optarg);
339 /* remove trailing directory separator */
340 while (optarglen > 0 &&
341 (optarg[optarglen - 1] == '/' ||
342 optarg[optarglen - 1] == '\\'))
343 optarg[--optarglen] = '\0';
344 dbpath = absolute_path(optarg);
345 if( dbpath == NULL)
346 fprintf(stderr, "#error: can not allocate memory for dbpath\n");
347 else
348 setlen = mo_add_option(&set, setlen, opt_cmdline, "gdk_dbpath", dbpath);
349 break;
350 }
351 if (strcmp(long_options[option_index].name, "dbextra") == 0) {
352 if (dbextra)
353 fprintf(stderr, "#warning: ignoring multiple --dbextra arguments\n");
354 else
355 dbextra = optarg;
356 break;
357 }
358 if (strcmp(long_options[option_index].name, "single-user") == 0) {
359 setlen = mo_add_option(&set, setlen, opt_cmdline, "gdk_single_user", "yes");
360 break;
361 }
362 if (strcmp(long_options[option_index].name, "version") == 0) {
363 monet_version();
364 exit(0);
365 }
366 /* debugging options */
367 if (strcmp(long_options[option_index].name, "properties") == 0) {
368 grpdebug |= GRPproperties;
369 break;
370 }
371 if (strcmp(long_options[option_index].name, "algorithms") == 0) {
372 grpdebug |= GRPalgorithms;
373 break;
374 }
375 if (strcmp(long_options[option_index].name, "optimizers") == 0) {
376 grpdebug |= GRPoptimizers;
377 break;
378 }
379 if (strcmp(long_options[option_index].name, "forcemito") == 0) {
380 grpdebug |= GRPforcemito;
381 break;
382 }
383 if (strcmp(long_options[option_index].name, "performance") == 0) {
384 grpdebug |= GRPperformance;
385 break;
386 }
387 if (strcmp(long_options[option_index].name, "io") == 0) {
388 grpdebug |= GRPio;
389 break;
390 }
391 if (strcmp(long_options[option_index].name, "memory") == 0) {
392 grpdebug |= GRPmemory;
393 break;
394 }
395 if (strcmp(long_options[option_index].name, "modules") == 0) {
396 grpdebug |= GRPmodules;
397 break;
398 }
399 if (strcmp(long_options[option_index].name, "transactions") == 0) {
400 grpdebug |= GRPtransactions;
401 break;
402 }
403 if (strcmp(long_options[option_index].name, "threads") == 0) {
404 grpdebug |= GRPthreads;
405 break;
406 }
407 if (strcmp(long_options[option_index].name, "heaps") == 0) {
408 grpdebug |= GRPheaps;
409 break;
410 }
411 usage(prog, -1);
412 /* not reached */
413 case 'c':
414 /* coverity[var_deref_model] */
415 setlen = mo_add_option(&set, setlen, opt_cmdline, "config", optarg);
416 break;
417 case 'd':
418 if (optarg) {
419 char *endarg;
420 debug |= strtol(optarg, &endarg, 10);
421 if (*endarg != '\0') {
422 fprintf(stderr, "ERROR: wrong format for --debug=%s\n",
423 optarg);
424 usage(prog, -1);
425 }
426 } else {
427 debug |= 1;
428 }
429 break;
430 case 'r':
431 setlen = mo_add_option(&set, setlen, opt_cmdline, "gdk_readonly", "yes");
432 break;
433 case 's': {
434 /* should add option to a list */
435 /* coverity[var_deref_model] */
436 char *tmp = strchr(optarg, '=');
437
438 if (tmp) {
439 *tmp = '\0';
440 setlen = mo_add_option(&set, setlen, opt_cmdline, optarg, tmp + 1);
441 } else
442 fprintf(stderr, "ERROR: wrong format %s\n", optarg);
443 }
444 break;
445 case 'v':
446 if (optarg) {
447 char *endarg;
448 verbosity = (int) strtol(optarg, &endarg, 10);
449 if (*endarg != '\0') {
450 fprintf(stderr, "ERROR: wrong format for --verbose=%s\n",
451 optarg);
452 usage(prog, -1);
453 }
454 } else {
455 verbosity++;
456 }
457 break;
458 case '?':
459 /* a bit of a hack: look at the option that the
460 current `c' is based on and see if we recognize
461 it: if -? or --help, exit with 0, else with -1 */
462 usage(prog, strcmp(av[optind - 1], "-?") == 0 || strcmp(av[optind - 1], "--help") == 0 ? 0 : -1);
463 default:
464 fprintf(stderr, "ERROR: getopt returned character "
465 "code '%c' 0%o\n", c, (uint8_t) c);
466 usage(prog, -1);
467 }
468 }
469
470 if (optind < argc)
471 usage(prog, -1);
472
473 if (!(setlen = mo_system_config(&set, setlen)))
474 usage(prog, -1);
475
476 GDKsetdebug(debug | grpdebug); /* add the algorithm tracers */
477 if (debug)
478 mo_print_options(set, setlen);
479 GDKsetverbose(verbosity);
480
481 if (dbpath && inmemory) {
482 fprintf(stderr, "!ERROR: both dbpath and in-memory must not be set at the same time\n");
483 exit(1);
484 }
485
486 if (!dbpath) {
487 dbpath = absolute_path(mo_find_option(set, setlen, "gdk_dbpath"));
488 if (!dbpath) {
489 fprintf(stderr, "!ERROR: cannot allocate memory for database directory \n");
490 exit(1);
491 }
492 }
493 if (inmemory) {
494 if (BBPaddfarm(NULL, (1 << PERSISTENT) | (1 << TRANSIENT)) != GDK_SUCCEED) {
495 fprintf(stderr, "!ERROR: cannot add in-memory farm\n");
496 exit(1);
497 }
498 } else {
499 if (BBPaddfarm(dbpath, 1 << PERSISTENT) != GDK_SUCCEED ||
500 BBPaddfarm(dbextra ? dbextra : dbpath, 1 << TRANSIENT) != GDK_SUCCEED) {
501 fprintf(stderr, "!ERROR: cannot add farm\n");
502 exit(1);
503 }
504 if (GDKcreatedir(dbpath) != GDK_SUCCEED) {
505 fprintf(stderr, "!ERROR: cannot create directory for %s\n", dbpath);
506 exit(1);
507 }
508 }
509 GDKfree(dbpath);
510 if (monet_init(set, setlen) == 0) {
511 mo_free_options(set, setlen);
512 if (GDKerrbuf && *GDKerrbuf)
513 fprintf(stderr, "%s\n", GDKerrbuf);
514 exit(1);
515 }
516 mo_free_options(set, setlen);
517
518 if (GDKsetenv("monet_version", GDKversion()) != GDK_SUCCEED ||
519 GDKsetenv("monet_release",
520#ifdef MONETDB_RELEASE
521 MONETDB_RELEASE
522#else
523 "unreleased"
524#endif
525 ) != GDK_SUCCEED) {
526 fprintf(stderr, "!ERROR: GDKsetenv failed\n");
527 exit(1);
528 }
529
530 if ((modpath = GDKgetenv("monet_mod_path")) == NULL) {
531 /* start probing based on some heuristics given the binary
532 * location:
533 * bin/mserver5 -> ../
534 * libX/monetdb5/lib/
535 * probe libX = lib, lib32, lib64, lib/64 */
536 size_t pref;
537 /* "remove" common prefix of configured BIN and LIB
538 * directories from LIBDIR */
539 for (pref = 0; LIBDIR[pref] != 0 && BINDIR[pref] == LIBDIR[pref]; pref++)
540 ;
541 const char *libdirs[] = {
542 &LIBDIR[pref],
543 "lib",
544 "lib64",
545 "lib/64",
546 "lib32",
547 NULL,
548 };
549 struct stat sb;
550 if (binpath != NULL) {
551 char *p = strrchr(binpath, DIR_SEP);
552 if (p != NULL)
553 *p = '\0';
554 p = strrchr(binpath, DIR_SEP);
555 if (p != NULL) {
556 *p = '\0';
557 for (int i = 0; libdirs[i] != NULL; i++) {
558 int len = snprintf(prmodpath, sizeof(prmodpath), "%s%c%s%cmonetdb5",
559 binpath, DIR_SEP, libdirs[i], DIR_SEP);
560 if (len == -1 || len >= FILENAME_MAX)
561 continue;
562 if (stat(prmodpath, &sb) == 0) {
563 modpath = prmodpath;
564 break;
565 }
566 }
567 } else {
568 printf("#warning: unusable binary location, "
569 "please use --set monet_mod_path=/path/to/... to "
570 "allow finding modules\n");
571 fflush(NULL);
572 }
573 } else {
574 printf("#warning: unable to determine binary location, "
575 "please use --set monet_mod_path=/path/to/... to "
576 "allow finding modules\n");
577 fflush(NULL);
578 }
579 if (modpath != NULL &&
580 GDKsetenv("monet_mod_path", modpath) != GDK_SUCCEED) {
581 fprintf(stderr, "!ERROR: GDKsetenv failed\n");
582 exit(1);
583 }
584 }
585
586 if (!GDKinmemory()) {
587 /* configure sabaoth to use the right dbpath and active database */
588 msab_dbpathinit(GDKgetenv("gdk_dbpath"));
589 /* wipe out all cruft, if left over */
590 if ((err = msab_wildRetreat()) != NULL) {
591 /* just swallow the error */
592 free(err);
593 }
594 /* From this point, the server should exit cleanly. Discussion:
595 * even earlier? Sabaoth here registers the server is starting up. */
596 if ((err = msab_registerStarting()) != NULL) {
597 /* throw the error at the user, but don't die */
598 fprintf(stderr, "!%s\n", err);
599 free(err);
600 }
601 }
602
603#ifdef HAVE_SIGACTION
604 {
605 struct sigaction sa;
606
607 (void) sigemptyset(&sa.sa_mask);
608 sa.sa_flags = 0;
609 sa.sa_handler = handler;
610 if (sigaction(SIGINT, &sa, NULL) == -1 ||
611 sigaction(SIGQUIT, &sa, NULL) == -1 ||
612 sigaction(SIGTERM, &sa, NULL) == -1) {
613 fprintf(stderr, "!unable to create signal handlers\n");
614 }
615 }
616#else
617#ifdef _MSC_VER
618 if (!SetConsoleCtrlHandler(winhandler, TRUE))
619 fprintf(stderr, "!unable to create console control handler\n");
620#else
621 if(signal(SIGINT, handler) == SIG_ERR)
622 fprintf(stderr, "!unable to create signal handlers\n");
623#ifdef SIGQUIT
624 if(signal(SIGQUIT, handler) == SIG_ERR)
625 fprintf(stderr, "!unable to create signal handlers\n");
626#endif
627 if(signal(SIGTERM, handler) == SIG_ERR)
628 fprintf(stderr, "!unable to create signal handlers\n");
629#endif
630#endif
631
632 if (!GDKinmemory()) {
633 str lang = "mal";
634 /* we inited mal before, so publish its existence */
635 if ((err = msab_marchScenario(lang)) != NULL) {
636 /* throw the error at the user, but don't die */
637 fprintf(stderr, "!%s\n", err);
638 free(err);
639 }
640 }
641
642 {
643 /* unlock the vault, first see if we can find the file which
644 * holds the secret */
645 char secret[1024];
646 char *secretp = secret;
647 FILE *secretf;
648 size_t len;
649
650 if (GDKinmemory() || GDKgetenv("monet_vault_key") == NULL) {
651 /* use a default (hard coded, non safe) key */
652 snprintf(secret, sizeof(secret), "%s", "Xas632jsi2whjds8");
653 } else {
654 if ((secretf = fopen(GDKgetenv("monet_vault_key"), "r")) == NULL) {
655 fprintf(stderr,
656 "unable to open vault_key_file %s: %s\n",
657 GDKgetenv("monet_vault_key"), strerror(errno));
658 /* don't show this as a crash */
659 msab_registerStop();
660 exit(1);
661 }
662 len = fread(secret, 1, sizeof(secret), secretf);
663 secret[len] = '\0';
664 len = strlen(secret); /* secret can contain null-bytes */
665 if (len == 0) {
666 fprintf(stderr, "vault key has zero-length!\n");
667 /* don't show this as a crash */
668 msab_registerStop();
669 exit(1);
670 } else if (len < 5) {
671 fprintf(stderr, "#warning: your vault key is too short "
672 "(%zu), enlarge your vault key!\n", len);
673 }
674 fclose(secretf);
675 }
676 if ((err = AUTHunlockVault(secretp)) != MAL_SUCCEED) {
677 /* don't show this as a crash */
678 if (!GDKinmemory())
679 msab_registerStop();
680 fprintf(stderr, "%s\n", err);
681 freeException(err);
682 exit(1);
683 }
684 }
685 /* make sure the authorisation BATs are loaded */
686 if ((err = AUTHinitTables(NULL)) != MAL_SUCCEED) {
687 /* don't show this as a crash */
688 if (!GDKinmemory())
689 msab_registerStop();
690 fprintf(stderr, "%s\n", err);
691 freeException(err);
692 exit(1);
693 }
694 if (mal_init()) {
695 /* don't show this as a crash */
696 if (!GDKinmemory())
697 msab_registerStop();
698 return 0;
699 }
700
701 emergencyBreakpoint();
702
703 if (!GDKinmemory() && (err = msab_registerStarted()) != NULL) {
704 /* throw the error at the user, but don't die */
705 fprintf(stderr, "!%s\n", err);
706 free(err);
707 }
708
709 /* why busy wait ? */
710 while (!interrupted && !GDKexiting()) {
711 MT_sleep_ms(100);
712 }
713
714 /* mal_exit calls exit, so statements after this call will
715 * never get reached */
716 mal_exit(0);
717
718 return 0;
719}
720