daemon: Support systemd-style socket activation.
[jackhill/guix/guix.git] / nix / nix-daemon / guix-daemon.cc
CommitLineData
233e7676 1/* GNU Guix --- Functional package management for GNU
5f74169e 2 Copyright (C) 2012-2019, 2021-2022 Ludovic Courtès <ludo@gnu.org>
1071f781 3 Copyright (C) 2006, 2010, 2012, 2014 Eelco Dolstra <e.dolstra@tudelft.nl>
c2033df4 4
233e7676 5 This file is part of GNU Guix.
c2033df4 6
233e7676 7 GNU Guix is free software; you can redistribute it and/or modify it
c2033df4
LC
8 under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 3 of the License, or (at
10 your option) any later version.
11
233e7676 12 GNU Guix is distributed in the hope that it will be useful, but
c2033df4
LC
13 WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
233e7676 18 along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. */
c2033df4
LC
19
20#include <config.h>
21
22#include <types.hh>
23#include "shared.hh"
24#include <globals.hh>
da30f555 25#include <util.hh>
c2033df4 26
5501e6b6
LC
27#include <gcrypt.h>
28
c2033df4
LC
29#include <stdlib.h>
30#include <argp.h>
e2332e8a
LC
31#include <unistd.h>
32#include <sys/types.h>
b49632e7 33#include <sys/stat.h>
1071f781
LC
34#include <sys/socket.h>
35#include <sys/un.h>
36#include <netdb.h>
6e37066e 37#include <strings.h>
868fce7c 38#include <exception>
1071f781 39#include <iostream>
c2033df4 40
7887bcbf
LC
41#include <libintl.h>
42#include <locale.h>
43
c2033df4
LC
44/* Variables used by `nix-daemon.cc'. */
45volatile ::sig_atomic_t blockInt;
46char **argvSaved;
47
48using namespace nix;
49
50/* Entry point in `nix-daemon.cc'. */
1071f781 51extern void run (const std::vector<int> &);
c2033df4
LC
52
53\f
54/* Command-line options. */
55
7887bcbf
LC
56#define n_(str) str
57#define _(str) gettext (str)
58static const char guix_textdomain[] = "guix";
59
60
c2033df4
LC
61const char *argp_program_version =
62 "guix-daemon (" PACKAGE_NAME ") " PACKAGE_VERSION;
63const char *argp_program_bug_address = PACKAGE_BUGREPORT;
64
65static char doc[] =
7887bcbf
LC
66 n_("guix-daemon -- perform derivation builds and store accesses")
67 "\v\n"
68 n_("This program is a daemon meant to run in the background. It serves \
c2033df4 69requests sent over a Unix-domain socket. It accesses the store, and \
7887bcbf 70builds derivations on behalf of its clients.");
c2033df4
LC
71
72#define GUIX_OPT_SYSTEM 1
73#define GUIX_OPT_DISABLE_CHROOT 2
5c403e35
LC
74#define GUIX_OPT_BUILD_USERS_GROUP 3
75#define GUIX_OPT_CACHE_FAILURES 4
76#define GUIX_OPT_LOSE_LOGS 5
77#define GUIX_OPT_DISABLE_LOG_COMPRESSION 6
ab3893d7 78#define GUIX_OPT_DISABLE_DEDUPLICATION 7
5c403e35 79#define GUIX_OPT_IMPERSONATE_LINUX_26 8
da30f555 80#define GUIX_OPT_DEBUG 9
72ce0373 81#define GUIX_OPT_CHROOT_DIR 10
b8d2aa26 82#define GUIX_OPT_LISTEN 11
6858f9d1 83#define GUIX_OPT_NO_SUBSTITUTES 12
9176607e
LC
84#define GUIX_OPT_SUBSTITUTE_URLS 13
85#define GUIX_OPT_NO_BUILD_HOOK 14
86#define GUIX_OPT_GC_KEEP_OUTPUTS 15
87#define GUIX_OPT_GC_KEEP_DERIVATIONS 16
ecf84b7c 88#define GUIX_OPT_BUILD_ROUNDS 17
2ca9f51e
LC
89#define GUIX_OPT_TIMEOUT 18
90#define GUIX_OPT_MAX_SILENT_TIME 19
29a68668 91#define GUIX_OPT_LOG_COMPRESSION 20
79f9dee3 92#define GUIX_OPT_DISCOVER 21
c2033df4
LC
93
94static const struct argp_option options[] =
95 {
7887bcbf
LC
96 { "system", GUIX_OPT_SYSTEM, n_("SYSTEM"), 0,
97 n_("assume SYSTEM as the current system type") },
98 { "cores", 'c', n_("N"), 0,
99 n_("use N CPU cores to build each derivation; 0 means as many as available")
100 },
101 { "max-jobs", 'M', n_("N"), 0,
102 n_("allow at most N build jobs") },
2ca9f51e
LC
103 { "timeout", GUIX_OPT_TIMEOUT, n_("SECONDS"), 0,
104 n_("mark builds as failed after SECONDS of activity") },
105 { "max-silent-time", GUIX_OPT_MAX_SILENT_TIME, n_("SECONDS"), 0,
106 n_("mark builds as failed after SECONDS of silence") },
c2033df4 107 { "disable-chroot", GUIX_OPT_DISABLE_CHROOT, 0, 0,
7887bcbf
LC
108 n_("disable chroot builds") },
109 { "chroot-directory", GUIX_OPT_CHROOT_DIR, n_("DIR"), 0,
110 n_("add DIR to the build chroot") },
111 { "build-users-group", GUIX_OPT_BUILD_USERS_GROUP, n_("GROUP"), 0,
112 n_("perform builds as a user of GROUP") },
6858f9d1 113 { "no-substitutes", GUIX_OPT_NO_SUBSTITUTES, 0, 0,
7887bcbf
LC
114 n_("do not use substitutes") },
115 { "substitute-urls", GUIX_OPT_SUBSTITUTE_URLS, n_("URLS"), 0,
116 n_("use URLS as the default list of substitute providers") },
dc209d5a
LC
117 { "no-offload", GUIX_OPT_NO_BUILD_HOOK, 0, 0,
118 n_("do not attempt to offload builds") },
119 { "no-build-hook", GUIX_OPT_NO_BUILD_HOOK, 0,
120 OPTION_HIDDEN, // deprecated
121 n_("do not attempt to offload builds") },
5c403e35 122 { "cache-failures", GUIX_OPT_CACHE_FAILURES, 0, 0,
7887bcbf 123 n_("cache build failures") },
ecf84b7c
LC
124 { "rounds", GUIX_OPT_BUILD_ROUNDS, "N", 0,
125 n_("build each derivation N times in a row") },
5c403e35 126 { "lose-logs", GUIX_OPT_LOSE_LOGS, 0, 0,
7887bcbf 127 n_("do not keep build logs") },
29a68668
LC
128 { "disable-log-compression", GUIX_OPT_DISABLE_LOG_COMPRESSION, 0,
129 OPTION_HIDDEN, // deprecated
7887bcbf 130 n_("disable compression of the build logs") },
29a68668
LC
131 { "log-compression", GUIX_OPT_LOG_COMPRESSION, "TYPE", 0,
132 n_("use the specified compression type for build logs") },
79f9dee3
MO
133 { "discover", GUIX_OPT_DISCOVER, "yes/no", OPTION_ARG_OPTIONAL,
134 n_("use substitute servers discovered on the local network") },
ab3893d7
LC
135
136 /* '--disable-deduplication' was known as '--disable-store-optimization'
137 up to Guix 0.7 included, so keep the alias around. */
138 { "disable-deduplication", GUIX_OPT_DISABLE_DEDUPLICATION, 0, 0,
7887bcbf 139 n_("disable automatic file \"deduplication\" in the store") },
ab3893d7
LC
140 { "disable-store-optimization", GUIX_OPT_DISABLE_DEDUPLICATION, 0,
141 OPTION_ALIAS | OPTION_HIDDEN, NULL },
142
7887bcbf
LC
143 { "impersonate-linux-2.6", GUIX_OPT_IMPERSONATE_LINUX_26, 0,
144#ifdef HAVE_SYS_PERSONALITY_H
145 0,
146#else
147 OPTION_HIDDEN,
5c403e35 148#endif
7887bcbf 149 n_("impersonate Linux 2.6")
5c403e35 150 },
6e37066e
LC
151 { "gc-keep-outputs", GUIX_OPT_GC_KEEP_OUTPUTS,
152 "yes/no", OPTION_ARG_OPTIONAL,
7887bcbf 153 n_("tell whether the GC must keep outputs of live derivations") },
6e37066e
LC
154 { "gc-keep-derivations", GUIX_OPT_GC_KEEP_DERIVATIONS,
155 "yes/no", OPTION_ARG_OPTIONAL,
7887bcbf
LC
156 n_("tell whether the GC must keep derivations corresponding \
157to live outputs") },
6e37066e 158
7887bcbf
LC
159 { "listen", GUIX_OPT_LISTEN, n_("SOCKET"), 0,
160 n_("listen for connections on SOCKET") },
da30f555 161 { "debug", GUIX_OPT_DEBUG, 0, 0,
7887bcbf 162 n_("produce debugging output") },
c2033df4
LC
163 { 0, 0, 0, 0, 0 }
164 };
165
6e37066e 166
1071f781
LC
167/* Default port for '--listen' on TCP/IP. */
168#define DEFAULT_GUIX_PORT "44146"
169
170/* List of '--listen' options. */
171static std::list<std::string> listen_options;
172
79f9dee3
MO
173static bool useDiscover = false;
174
6e37066e
LC
175/* Convert ARG to a Boolean value, or throw an error if it does not denote a
176 Boolean. */
177static bool
178string_to_bool (const char *arg, bool dflt = true)
179{
180 if (arg == NULL)
181 return dflt;
182 else if (strcasecmp (arg, "yes") == 0)
183 return true;
184 else if (strcasecmp (arg, "no") == 0)
185 return false;
186 else
187 throw nix::Error (format ("'%1%': invalid Boolean value") % arg);
188}
189
c2033df4
LC
190/* Parse a single option. */
191static error_t
192parse_opt (int key, char *arg, struct argp_state *state)
193{
194 switch (key)
195 {
196 case GUIX_OPT_DISABLE_CHROOT:
197 settings.useChroot = false;
198 break;
72ce0373 199 case GUIX_OPT_CHROOT_DIR:
2bb04905
LC
200 {
201 std::string chroot_dirs;
202
203 chroot_dirs = settings.get ("build-extra-chroot-dirs",
204 (std::string) "");
205 if (chroot_dirs == "")
206 chroot_dirs = arg;
207 else
208 chroot_dirs = chroot_dirs + " " + arg;
209 settings.set("build-extra-chroot-dirs", chroot_dirs);
210 break;
211 }
29a68668
LC
212 case GUIX_OPT_LOG_COMPRESSION:
213 if (strcmp (arg, "none") == 0)
214 settings.logCompression = COMPRESSION_NONE;
215 else if (strcmp (arg, "gzip") == 0)
216 settings.logCompression = COMPRESSION_GZIP;
f997137d 217#if HAVE_BZLIB_H
29a68668
LC
218 else if (strcmp (arg, "bzip2") == 0)
219 settings.logCompression = COMPRESSION_BZIP2;
f997137d 220#endif
29a68668
LC
221 else
222 {
223 fprintf (stderr, _("error: %s: unknown compression type\n"), arg);
224 exit (EXIT_FAILURE);
225 }
226 break;
c2033df4 227 case GUIX_OPT_DISABLE_LOG_COMPRESSION:
29a68668 228 settings.logCompression = COMPRESSION_NONE;
c2033df4 229 break;
5c403e35
LC
230 case GUIX_OPT_BUILD_USERS_GROUP:
231 settings.buildUsersGroup = arg;
232 break;
ab3893d7 233 case GUIX_OPT_DISABLE_DEDUPLICATION:
5c403e35
LC
234 settings.autoOptimiseStore = false;
235 break;
236 case GUIX_OPT_CACHE_FAILURES:
237 settings.cacheFailure = true;
238 break;
ecf84b7c
LC
239 case GUIX_OPT_BUILD_ROUNDS:
240 {
241 char *end;
242 unsigned long n = strtoul (arg, &end, 10);
243 if (end != arg + strlen (arg))
244 {
245 fprintf (stderr, _("error: %s: invalid number of rounds\n"), arg);
246 exit (EXIT_FAILURE);
247 }
248 settings.set ("build-repeat", std::to_string (std::max (0UL, n - 1)));
249 break;
250 }
5c403e35
LC
251 case GUIX_OPT_IMPERSONATE_LINUX_26:
252 settings.impersonateLinux26 = true;
253 break;
254 case GUIX_OPT_LOSE_LOGS:
255 settings.keepLog = false;
256 break;
b8d2aa26 257 case GUIX_OPT_LISTEN:
1071f781 258 listen_options.push_back (arg);
b8d2aa26 259 break;
9176607e
LC
260 case GUIX_OPT_SUBSTITUTE_URLS:
261 settings.set ("substitute-urls", arg);
262 break;
6858f9d1 263 case GUIX_OPT_NO_SUBSTITUTES:
ad0ab74e 264 settings.set ("build-use-substitutes", "false");
6858f9d1 265 break;
49e6291a
LC
266 case GUIX_OPT_NO_BUILD_HOOK:
267 settings.useBuildHook = false;
268 break;
79f9dee3
MO
269 case GUIX_OPT_DISCOVER:
270 useDiscover = string_to_bool (arg);
bc3896db 271 settings.set ("discover", useDiscover ? "true" : "false");
79f9dee3 272 break;
da30f555
LC
273 case GUIX_OPT_DEBUG:
274 verbosity = lvlDebug;
275 break;
6e37066e
LC
276 case GUIX_OPT_GC_KEEP_OUTPUTS:
277 settings.gcKeepOutputs = string_to_bool (arg);
278 break;
279 case GUIX_OPT_GC_KEEP_DERIVATIONS:
280 settings.gcKeepDerivations = string_to_bool (arg);
281 break;
6221db61 282 case 'c':
ad0ab74e 283 settings.set ("build-cores", arg);
c2033df4
LC
284 break;
285 case 'M':
ad0ab74e 286 settings.set ("build-max-jobs", arg);
c2033df4 287 break;
2ca9f51e
LC
288 case GUIX_OPT_TIMEOUT:
289 settings.set ("build-timeout", arg);
290 break;
291 case GUIX_OPT_MAX_SILENT_TIME:
292 settings.set ("build-max-silent-time", arg);
293 break;
c2033df4
LC
294 case GUIX_OPT_SYSTEM:
295 settings.thisSystem = arg;
296 break;
297 default:
5363abb7 298 return (error_t) ARGP_ERR_UNKNOWN;
c2033df4
LC
299 }
300
5363abb7 301 return (error_t) 0;
c2033df4
LC
302}
303
304/* Argument parsing. */
7887bcbf
LC
305static const struct argp argp =
306 {
307 options, parse_opt,
308 NULL, doc,
309 NULL, NULL, // children and help_filter
310 guix_textdomain
311 };
c2033df4 312
1071f781
LC
313\f
314static int
315open_unix_domain_socket (const char *file)
316{
317 /* Create and bind to a Unix domain socket. */
318 AutoCloseFD fdSocket = socket (PF_UNIX, SOCK_STREAM, 0);
319 if (fdSocket == -1)
320 throw SysError (_("cannot create Unix domain socket"));
321
322 createDirs (dirOf (file));
323
324 /* Urgh, sockaddr_un allows path names of only 108 characters.
325 So chdir to the socket directory so that we can pass a
326 relative path name. */
327 if (chdir (dirOf (file).c_str ()) == -1)
328 throw SysError (_("cannot change current directory"));
329 Path fileRel = "./" + baseNameOf (file);
330
331 struct sockaddr_un addr;
332 addr.sun_family = AF_UNIX;
333 if (fileRel.size () >= sizeof (addr.sun_path))
334 throw Error (format (_("socket file name '%1%' is too long")) % fileRel);
335 strcpy (addr.sun_path, fileRel.c_str ());
336
337 unlink (file);
338
339 /* Make sure that the socket is created with 0666 permission
340 (everybody can connect --- provided they have access to the
341 directory containing the socket). */
342 mode_t oldMode = umask (0111);
343 int res = bind (fdSocket, (struct sockaddr *) &addr, sizeof addr);
344 umask (oldMode);
345 if (res == -1)
346 throw SysError (format (_("cannot bind to socket '%1%'")) % file);
347
348 if (chdir ("/") == -1) /* back to the root */
349 throw SysError (_("cannot change current directory"));
350
351 if (listen (fdSocket, 5) == -1)
352 throw SysError (format (_("cannot listen on socket '%1%'")) % file);
353
354 return fdSocket.borrow ();
355}
356
357/* Return a listening socket for ADDRESS, which has the given LENGTH. */
358static int
359open_inet_socket (const struct sockaddr *address, socklen_t length)
360{
361 AutoCloseFD fd = socket (address->sa_family, SOCK_STREAM, 0);
362 if (fd == -1)
363 throw SysError (_("cannot create TCP socket"));
364
365 int res = bind (fd, address, length);
366 if (res == -1)
367 throw SysError (_("cannot bind TCP socket"));
368
369 if (listen (fd, 5) == -1)
370 throw SysError (format (_("cannot listen on TCP socket")));
371
372 return fd.borrow ();
373}
374
375/* Return a list of file descriptors of listening sockets. */
376static std::vector<int>
377listening_sockets (const std::list<std::string> &options)
378{
379 std::vector<int> result;
380
381 if (options.empty ())
382 {
383 /* Open the default Unix-domain socket. */
384 auto fd = open_unix_domain_socket (settings.nixDaemonSocketFile.c_str ());
385 result.push_back (fd);
386 return result;
387 }
388
389 /* Open the user-specified sockets. */
390 for (const std::string& option: options)
391 {
392 if (option[0] == '/')
393 {
394 /* Assume OPTION is the file name of a Unix-domain socket. */
395 settings.nixDaemonSocketFile = canonPath (option);
396 int fd =
397 open_unix_domain_socket (settings.nixDaemonSocketFile.c_str ());
398 result.push_back (fd);
399 }
400 else
401 {
402 /* Assume OPTIONS has the form "HOST" or "HOST:PORT". */
403 auto colon = option.find_last_of (":");
404 auto host = colon == std::string::npos
405 ? option : option.substr (0, colon);
406 auto port = colon == std::string::npos
407 ? DEFAULT_GUIX_PORT
408 : option.substr (colon + 1, option.size () - colon - 1);
409
410 struct addrinfo *res, hints;
411
412 memset (&hints, '\0', sizeof hints);
413 hints.ai_socktype = SOCK_STREAM;
414 hints.ai_flags = AI_NUMERICSERV | AI_ADDRCONFIG;
415
416 int err = getaddrinfo (host.c_str(), port.c_str (),
417 &hints, &res);
418
419 if (err != 0)
420 throw Error(format ("failed to look up '%1%': %2%")
421 % option % gai_strerror (err));
422
423 printMsg (lvlDebug, format ("listening on '%1%', port '%2%'")
424 % host % port);
425
426 /* XXX: Pick the first result, RES. */
427 result.push_back (open_inet_socket (res->ai_addr,
428 res->ai_addrlen));
429
430 freeaddrinfo (res);
431 }
432 }
433
434 return result;
435}
c2033df4 436
5f74169e
LC
437/* First file descriptor provided at startup using systemd-style socket
438 activation. */
439#define SD_LISTEN_FDS_START 3
440
441/* Return a list of file descriptors of listening sockets provided following
442 the systemd "socket activation" protocol. Return the empty list if we are
443 not being socket-activated. */
444static std::vector<int>
445systemd_activation_sockets ()
446{
447 std::vector<int> result;
448
449 if (getEnv ("LISTEN_PID") == std::to_string (getpid ()))
450 {
451 unsigned int fdCount;
452 if (string2Int (getEnv ("LISTEN_FDS"), fdCount))
453 {
454 for (unsigned int i = 0; i < fdCount; i++)
455 result.push_back (SD_LISTEN_FDS_START + i);
456 }
457 }
458
459 return result;
460}
461
c2033df4
LC
462\f
463int
464main (int argc, char *argv[])
465{
7887bcbf
LC
466 setlocale (LC_ALL, "");
467 bindtextdomain (guix_textdomain, LOCALEDIR);
468 textdomain (guix_textdomain);
c2033df4 469
5501e6b6
LC
470 /* Initialize libgcrypt. */
471 if (!gcry_check_version (GCRYPT_VERSION))
472 {
7887bcbf 473 fprintf (stderr, _("error: libgcrypt version mismatch\n"));
5501e6b6
LC
474 exit (EXIT_FAILURE);
475 }
476
37dd969c
LC
477 /* Tell Libgcrypt that initialization has completed, as per the Libgcrypt
478 1.6.0 manual (although this does not appear to be strictly needed.) */
479 gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
480
b49632e7
LC
481 /* Set the umask so that the daemon does not end up creating group-writable
482 files, which would lead to "suspicious ownership or permission" errors.
483 See <http://lists.gnu.org/archive/html/bug-guix/2013-07/msg00033.html>. */
484 umask (S_IWGRP | S_IWOTH);
485
3b9855f4
LC
486#ifndef HAVE_CHROOT
487# error chroot is assumed to be available
8b15ac67
LC
488#endif
489
3b9855f4
LC
490 /* Always use chroots by default. */
491 settings.useChroot = true;
492
44d43c7a
LC
493 /* Turn automatic deduplication on by default. */
494 settings.autoOptimiseStore = true;
495
deac976d 496 /* Default to using as many cores as possible and one job at a time. */
6efc160e 497 settings.buildCores = 0;
deac976d 498 settings.maxBuildJobs = 1;
6efc160e 499
868fce7c
LC
500 argvSaved = argv;
501
502 try
503 {
504 settings.processEnvironment ();
c2033df4 505
f6919ebd 506 /* Enable substitutes by default. */
ad0ab74e 507 settings.set ("build-use-substitutes", "true");
335dd762 508
9176607e 509 /* Use our substitute server by default. */
df061d07 510 settings.set ("substitute-urls", GUIX_SUBSTITUTE_URLS);
9176607e 511
49e6291a 512#ifdef HAVE_DAEMON_OFFLOAD_HOOK
bc69ea2d 513 /* Use 'guix offload' for distributed builds by default. */
49e6291a 514 settings.useBuildHook = true;
49e6291a
LC
515#else
516 /* We are not installing any build hook, so disable it. */
517 settings.useBuildHook = false;
518#endif
519
868fce7c 520 argp_parse (&argp, argc, argv, 0, 0, 0);
c2033df4 521
5f74169e
LC
522 auto sockets = systemd_activation_sockets ();
523 if (sockets.empty ())
524 /* We were not "socket-activated" so open the sockets specified by
525 LISTEN_OPTIONS. */
526 sockets = listening_sockets (listen_options);
527 else
528 printMsg (lvlInfo,
529 format (ngettext ("socket-activated with %1% socket",
530 "socket-activated with %1% sockets",
531 sockets.size ()))
532 % sockets.size ());
1071f781 533
ad0ab74e
LC
534 /* Effect all the changes made via 'settings.set'. */
535 settings.update ();
29a68668
LC
536 printMsg(lvlDebug,
537 format ("build log compression: %1%") % settings.logCompression);
ad0ab74e 538
868fce7c 539 if (geteuid () == 0 && settings.buildUsersGroup.empty ())
7887bcbf
LC
540 fprintf (stderr, _("warning: daemon is running as root, so \
541using `--build-users-group' is highly recommended\n"));
e2332e8a 542
706d0641
LC
543 if (settings.useChroot)
544 {
2bb04905
LC
545 std::string chroot_dirs;
546
547 chroot_dirs = settings.get ("build-extra-chroot-dirs",
548 (std::string) "");
549 printMsg (lvlDebug,
550 format ("extra chroot directories: '%1%'") % chroot_dirs);
706d0641 551 }
706d0641 552
79f9dee3
MO
553 if (useDiscover)
554 {
555 Strings args;
556
557 args.push_back("guix");
558 args.push_back("discover");
559
560 startProcess([&]() {
561 execv(settings.guixProgram.c_str(), stringsToCharPtrs(args).data());
562 });
563 }
564
44d43c7a
LC
565 printMsg (lvlDebug,
566 format ("automatic deduplication set to %1%")
567 % settings.autoOptimiseStore);
568
1071f781 569 run (sockets);
868fce7c
LC
570 }
571 catch (std::exception &e)
572 {
7887bcbf 573 fprintf (stderr, _("error: %s\n"), e.what ());
868fce7c
LC
574 return EXIT_FAILURE;
575 }
576
577 return EXIT_SUCCESS; /* never reached */
c2033df4 578}