chiark / gitweb /
enable localization for common *ctl commands
[elogind.git] / src / locale / localectl.c
1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3 /***
4   This file is part of systemd.
5
6   Copyright 2012 Lennart Poettering
7
8   systemd is free software; you can redistribute it and/or modify it
9   under the terms of the GNU Lesser General Public License as published by
10   the Free Software Foundation; either version 2.1 of the License, or
11   (at your option) any later version.
12
13   systemd is distributed in the hope that it will be useful, but
14   WITHOUT ANY WARRANTY; without even the implied warranty of
15   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16   Lesser General Public License for more details.
17
18   You should have received a copy of the GNU Lesser General Public License
19   along with systemd; If not, see <http://www.gnu.org/licenses/>.
20 ***/
21
22 #include <locale.h>
23 #include <stdlib.h>
24 #include <stdbool.h>
25 #include <unistd.h>
26 #include <getopt.h>
27 #include <string.h>
28 #include <ftw.h>
29 #include <sys/mman.h>
30 #include <fcntl.h>
31
32 #include "dbus-common.h"
33 #include "util.h"
34 #include "spawn-polkit-agent.h"
35 #include "build.h"
36 #include "strv.h"
37 #include "pager.h"
38 #include "set.h"
39 #include "path-util.h"
40
41 static bool arg_no_pager = false;
42 static enum transport {
43         TRANSPORT_NORMAL,
44         TRANSPORT_SSH,
45         TRANSPORT_POLKIT
46 } arg_transport = TRANSPORT_NORMAL;
47 static bool arg_ask_password = true;
48 static const char *arg_host = NULL;
49 static bool arg_convert = true;
50
51 static void pager_open_if_enabled(void) {
52
53         if (arg_no_pager)
54                 return;
55
56         pager_open();
57 }
58
59 static void polkit_agent_open_if_enabled(void) {
60
61         /* Open the polkit agent as a child process if necessary */
62
63         if (!arg_ask_password)
64                 return;
65
66         polkit_agent_open();
67 }
68
69 typedef struct StatusInfo {
70         char **locale;
71         const char *vconsole_keymap;
72         const char *vconsole_keymap_toggle;
73         const char *x11_layout;
74         const char *x11_model;
75         const char *x11_variant;
76         const char *x11_options;
77 } StatusInfo;
78
79 static void print_status_info(StatusInfo *i) {
80         assert(i);
81
82         if (strv_isempty(i->locale))
83                 puts("   System Locale: n/a\n");
84         else {
85                 char **j;
86
87                 printf("   System Locale: %s\n", i->locale[0]);
88                 STRV_FOREACH(j, i->locale + 1)
89                         printf("                  %s\n", *j);
90         }
91
92         printf("       VC Keymap: %s\n", strna(i->vconsole_keymap));
93         if (!isempty(i->vconsole_keymap_toggle))
94                 printf("VC Toggle Keymap: %s\n", i->vconsole_keymap_toggle);
95
96         printf("      X11 Layout: %s\n", strna(i->x11_layout));
97         if (!isempty(i->x11_model))
98                 printf("       X11 Model: %s\n", i->x11_model);
99         if (!isempty(i->x11_variant))
100                 printf("     X11 Variant: %s\n", i->x11_variant);
101         if (!isempty(i->x11_options))
102                 printf("     X11 Options: %s\n", i->x11_options);
103 }
104
105 static int status_property(const char *name, DBusMessageIter *iter, StatusInfo *i) {
106         int r;
107
108         assert(name);
109         assert(iter);
110
111         switch (dbus_message_iter_get_arg_type(iter)) {
112
113         case DBUS_TYPE_STRING: {
114                 const char *s;
115
116                 dbus_message_iter_get_basic(iter, &s);
117                 if (!isempty(s)) {
118                         if (streq(name, "VConsoleKeymap"))
119                                 i->vconsole_keymap = s;
120                         else if (streq(name, "VConsoleKeymapToggle"))
121                                 i->vconsole_keymap_toggle = s;
122                         else if (streq(name, "X11Layout"))
123                                 i->x11_layout = s;
124                         else if (streq(name, "X11Model"))
125                                 i->x11_model = s;
126                         else if (streq(name, "X11Variant"))
127                                 i->x11_variant = s;
128                         else if (streq(name, "X11Options"))
129                                 i->x11_options = s;
130                 }
131                 break;
132         }
133
134         case DBUS_TYPE_ARRAY:
135
136                 if (dbus_message_iter_get_element_type(iter) == DBUS_TYPE_STRING) {
137                         char **l;
138
139                         r = bus_parse_strv_iter(iter, &l);
140                         if (r < 0)
141                                 return r;
142
143                         if (streq(name, "Locale")) {
144                                 strv_free(i->locale);
145                                 i->locale = l;
146                                 l = NULL;
147                         }
148
149                         strv_free(l);
150                 }
151         }
152
153         return 0;
154 }
155
156 static int show_status(DBusConnection *bus, char **args, unsigned n) {
157         _cleanup_dbus_message_unref_ DBusMessage *reply = NULL;
158         const char *interface = "";
159         int r;
160         DBusMessageIter iter, sub, sub2, sub3;
161         StatusInfo info;
162
163         assert(args);
164
165         r = bus_method_call_with_reply(
166                         bus,
167                         "org.freedesktop.locale1",
168                         "/org/freedesktop/locale1",
169                         "org.freedesktop.DBus.Properties",
170                         "GetAll",
171                         &reply,
172                         NULL,
173                         DBUS_TYPE_STRING, &interface,
174                         DBUS_TYPE_INVALID);
175         if (r < 0)
176                 return r;
177
178         if (!dbus_message_iter_init(reply, &iter) ||
179             dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY ||
180             dbus_message_iter_get_element_type(&iter) != DBUS_TYPE_DICT_ENTRY)  {
181                 log_error("Failed to parse reply.");
182                 return -EIO;
183         }
184
185         zero(info);
186         dbus_message_iter_recurse(&iter, &sub);
187
188         while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) {
189                 const char *name;
190
191                 if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_DICT_ENTRY) {
192                         log_error("Failed to parse reply.");
193                         return -EIO;
194                 }
195
196                 dbus_message_iter_recurse(&sub, &sub2);
197
198                 if (bus_iter_get_basic_and_next(&sub2, DBUS_TYPE_STRING, &name, true) < 0) {
199                         log_error("Failed to parse reply.");
200                         return -EIO;
201                 }
202
203                 if (dbus_message_iter_get_arg_type(&sub2) != DBUS_TYPE_VARIANT)  {
204                         log_error("Failed to parse reply.");
205                         return -EIO;
206                 }
207
208                 dbus_message_iter_recurse(&sub2, &sub3);
209
210                 r = status_property(name, &sub3, &info);
211                 if (r < 0) {
212                         log_error("Failed to parse reply.");
213                         return r;
214                 }
215
216                 dbus_message_iter_next(&sub);
217         }
218
219         print_status_info(&info);
220         strv_free(info.locale);
221         return 0;
222 }
223
224 static int set_locale(DBusConnection *bus, char **args, unsigned n) {
225         _cleanup_dbus_message_unref_ DBusMessage *m = NULL, *reply = NULL;
226         dbus_bool_t interactive = true;
227         DBusError error;
228         DBusMessageIter iter;
229         int r;
230
231         assert(bus);
232         assert(args);
233
234         dbus_error_init(&error);
235
236         polkit_agent_open_if_enabled();
237
238         m = dbus_message_new_method_call(
239                         "org.freedesktop.locale1",
240                         "/org/freedesktop/locale1",
241                         "org.freedesktop.locale1",
242                         "SetLocale");
243         if (!m)
244                 return log_oom();
245
246         dbus_message_iter_init_append(m, &iter);
247
248         r = bus_append_strv_iter(&iter, args + 1);
249         if (r < 0)
250                 return log_oom();
251
252         if (!dbus_message_iter_append_basic(&iter, DBUS_TYPE_BOOLEAN, &interactive))
253                 return log_oom();
254
255         reply = dbus_connection_send_with_reply_and_block(bus, m, -1, &error);
256         if (!reply) {
257                 log_error("Failed to issue method call: %s", bus_error_message(&error));
258                 r = -EIO;
259                 goto finish;
260         }
261
262         r = 0;
263
264 finish:
265         dbus_error_free(&error);
266         return r;
267 }
268
269 static int list_locales(DBusConnection *bus, char **args, unsigned n) {
270         /* Stolen from glibc... */
271
272         struct locarhead {
273                 uint32_t magic;
274                 /* Serial number.  */
275                 uint32_t serial;
276                 /* Name hash table.  */
277                 uint32_t namehash_offset;
278                 uint32_t namehash_used;
279                 uint32_t namehash_size;
280                 /* String table.  */
281                 uint32_t string_offset;
282                 uint32_t string_used;
283                 uint32_t string_size;
284                 /* Table with locale records.  */
285                 uint32_t locrectab_offset;
286                 uint32_t locrectab_used;
287                 uint32_t locrectab_size;
288                 /* MD5 sum hash table.  */
289                 uint32_t sumhash_offset;
290                 uint32_t sumhash_used;
291                 uint32_t sumhash_size;
292         };
293
294         struct namehashent {
295                 /* Hash value of the name.  */
296                 uint32_t hashval;
297                 /* Offset of the name in the string table.  */
298                 uint32_t name_offset;
299                 /* Offset of the locale record.  */
300                 uint32_t locrec_offset;
301         };
302
303         const struct locarhead *h;
304         const struct namehashent *e;
305         const void *p = MAP_FAILED;
306         _cleanup_close_ int fd = -1;
307         _cleanup_strv_free_ char **l = NULL;
308         char **j;
309         Set *locales;
310         size_t sz = 0;
311         struct stat st;
312         unsigned i;
313         int r;
314
315         locales = set_new(string_hash_func, string_compare_func);
316         if (!locales)
317                 return log_oom();
318
319         fd = open("/usr/lib/locale/locale-archive", O_RDONLY|O_NOCTTY|O_CLOEXEC);
320         if (fd < 0) {
321                 log_error("Failed to open locale archive: %m");
322                 r = -errno;
323                 goto finish;
324         }
325
326         if (fstat(fd, &st) < 0) {
327                 log_error("fstat() failed: %m");
328                 r = -errno;
329                 goto finish;
330         }
331
332         if (!S_ISREG(st.st_mode)) {
333                 log_error("Archive file is not regular");
334                 r = -EBADMSG;
335                 goto finish;
336         }
337
338         if (st.st_size < (off_t) sizeof(struct locarhead)) {
339                 log_error("Archive has invalid size");
340                 r = -EBADMSG;
341                 goto finish;
342         }
343
344         p = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
345         if (p == MAP_FAILED) {
346                 log_error("Failed to map archive: %m");
347                 r = -errno;
348                 goto finish;
349         }
350
351         h = (const struct locarhead *) p;
352         if (h->magic != 0xde020109 ||
353             h->namehash_offset + h->namehash_size > st.st_size ||
354             h->string_offset + h->string_size > st.st_size ||
355             h->locrectab_offset + h->locrectab_size > st.st_size ||
356             h->sumhash_offset + h->sumhash_size > st.st_size) {
357                 log_error("Invalid archive file.");
358                 r = -EBADMSG;
359                 goto finish;
360         }
361
362         e = (const struct namehashent*) ((const uint8_t*) p + h->namehash_offset);
363         for (i = 0; i < h->namehash_size; i++) {
364                 char *z;
365
366                 if (e[i].locrec_offset == 0)
367                         continue;
368
369                 z = strdup((char*) p + e[i].name_offset);
370                 if (!z) {
371                         r = log_oom();
372                         goto finish;
373                 }
374
375                 r = set_put(locales, z);
376                 if (r < 0) {
377                         free(z);
378                         log_error("Failed to add locale: %s", strerror(-r));
379                         goto finish;
380                 }
381         }
382
383         l = set_get_strv(locales);
384         if (!l) {
385                 r = log_oom();
386                 goto finish;
387         }
388
389         set_free(locales);
390         locales = NULL;
391
392         strv_sort(l);
393
394         pager_open_if_enabled();
395
396         STRV_FOREACH(j, l)
397                 puts(*j);
398
399         r = 0;
400
401 finish:
402         if (p != MAP_FAILED)
403                 munmap((void*) p, sz);
404
405         set_free_free(locales);
406
407         return r;
408 }
409
410 static int set_vconsole_keymap(DBusConnection *bus, char **args, unsigned n) {
411         _cleanup_dbus_message_unref_ DBusMessage *reply = NULL;
412         dbus_bool_t interactive = true, b;
413         const char *map, *toggle_map;
414
415         assert(bus);
416         assert(args);
417
418         if (n > 3) {
419                 log_error("Too many arguments.");
420                 return -EINVAL;
421         }
422
423         polkit_agent_open_if_enabled();
424
425         map = args[1];
426         toggle_map = n > 2 ? args[2] : "";
427         b = arg_convert;
428
429         return bus_method_call_with_reply(
430                         bus,
431                         "org.freedesktop.locale1",
432                         "/org/freedesktop/locale1",
433                         "org.freedesktop.locale1",
434                         "SetVConsoleKeyboard",
435                         &reply,
436                         NULL,
437                         DBUS_TYPE_STRING, &map,
438                         DBUS_TYPE_STRING, &toggle_map,
439                         DBUS_TYPE_BOOLEAN, &b,
440                         DBUS_TYPE_BOOLEAN, &interactive,
441                         DBUS_TYPE_INVALID);
442 }
443
444 static Set *keymaps = NULL;
445
446 static int nftw_cb(
447                 const char *fpath,
448                 const struct stat *sb,
449                 int tflag,
450                 struct FTW *ftwbuf) {
451
452         char *p, *e;
453         int r;
454
455         if (tflag != FTW_F)
456                 return 0;
457
458         if (!endswith(fpath, ".map") &&
459             !endswith(fpath, ".map.gz"))
460                 return 0;
461
462         p = strdup(path_get_file_name(fpath));
463         if (!p)
464                 return log_oom();
465
466         e = endswith(p, ".map");
467         if (e)
468                 *e = 0;
469
470         e = endswith(p, ".map.gz");
471         if (e)
472                 *e = 0;
473
474         r = set_put(keymaps, p);
475         if (r == -EEXIST)
476                 free(p);
477         else if (r < 0) {
478                 log_error("Can't add keymap: %s", strerror(-r));
479                 free(p);
480                 return r;
481         }
482
483         return 0;
484 }
485
486 static int list_vconsole_keymaps(DBusConnection *bus, char **args, unsigned n) {
487         char _cleanup_strv_free_ **l = NULL;
488         char **i;
489
490         keymaps = set_new(string_hash_func, string_compare_func);
491         if (!keymaps)
492                 return log_oom();
493
494         nftw("/usr/share/kbd/keymaps/", nftw_cb, 20, FTW_MOUNT|FTW_PHYS);
495         nftw("/usr/lib/kbd/keymaps/", nftw_cb, 20, FTW_MOUNT|FTW_PHYS);
496         nftw("/lib/kbd/keymaps/", nftw_cb, 20, FTW_MOUNT|FTW_PHYS);
497
498         l = set_get_strv(keymaps);
499         if (!l) {
500                 set_free_free(keymaps);
501                 return log_oom();
502         }
503
504         set_free(keymaps);
505
506         if (strv_isempty(l)) {
507                 log_error("Couldn't find any console keymaps.");
508                 return -ENOENT;
509         }
510
511         strv_sort(l);
512
513         pager_open_if_enabled();
514
515         STRV_FOREACH(i, l)
516                 puts(*i);
517
518
519         return 0;
520 }
521
522 static int set_x11_keymap(DBusConnection *bus, char **args, unsigned n) {
523         _cleanup_dbus_message_unref_ DBusMessage *reply = NULL;
524         dbus_bool_t interactive = true, b;
525         const char *layout, *model, *variant, *options;
526
527         assert(bus);
528         assert(args);
529
530         if (n > 5) {
531                 log_error("Too many arguments.");
532                 return -EINVAL;
533         }
534
535         polkit_agent_open_if_enabled();
536
537         layout = args[1];
538         model = n > 2 ? args[2] : "";
539         variant = n > 3 ? args[3] : "";
540         options = n > 3 ? args[4] : "";
541         b = arg_convert;
542
543         return bus_method_call_with_reply(
544                         bus,
545                         "org.freedesktop.locale1",
546                         "/org/freedesktop/locale1",
547                         "org.freedesktop.locale1",
548                         "SetX11Keyboard",
549                         &reply,
550                         NULL,
551                         DBUS_TYPE_STRING, &layout,
552                         DBUS_TYPE_STRING, &model,
553                         DBUS_TYPE_STRING, &variant,
554                         DBUS_TYPE_STRING, &options,
555                         DBUS_TYPE_BOOLEAN, &b,
556                         DBUS_TYPE_BOOLEAN, &interactive,
557                         DBUS_TYPE_INVALID);
558 }
559
560 static int help(void) {
561
562         printf("%s [OPTIONS...] COMMAND ...\n\n"
563                "Query or change system time and date settings.\n\n"
564                "  -h --help              Show this help\n"
565                "     --version           Show package version\n"
566                "     --no-convert        Don't convert keyboard mappings\n"
567                "     --no-pager          Do not pipe output into a pager\n"
568                "     --no-ask-password   Do not prompt for password\n"
569                "  -H --host=[USER@]HOST  Operate on remote host\n\n"
570                "Commands:\n"
571                "  status                 Show current locale settings\n"
572                "  set-locale LOCALE...   Set system locale\n"
573                "  list-locales           Show known locales\n"
574                "  set-keymap MAP [MAP]   Set virtual console keyboard mapping\n"
575                "  list-keymaps           Show known virtual console keyboard mappings\n"
576                "  set-x11-keymap LAYOUT [MODEL] [VARIANT] [OPTIONS]\n"
577                "                         Set X11 keyboard mapping\n",
578                program_invocation_short_name);
579
580         return 0;
581 }
582
583 static int parse_argv(int argc, char *argv[]) {
584
585         enum {
586                 ARG_VERSION = 0x100,
587                 ARG_NO_PAGER,
588                 ARG_NO_CONVERT,
589                 ARG_NO_ASK_PASSWORD
590         };
591
592         static const struct option options[] = {
593                 { "help",                no_argument,       NULL, 'h'                     },
594                 { "version",             no_argument,       NULL, ARG_VERSION             },
595                 { "no-pager",            no_argument,       NULL, ARG_NO_PAGER            },
596                 { "host",                required_argument, NULL, 'H'                     },
597                 { "privileged",          no_argument,       NULL, 'P'                     },
598                 { "no-ask-password",     no_argument,       NULL, ARG_NO_ASK_PASSWORD     },
599                 { "no-convert",          no_argument,       NULL, ARG_NO_CONVERT          },
600                 { NULL,                  0,                 NULL, 0                       }
601         };
602
603         int c;
604
605         assert(argc >= 0);
606         assert(argv);
607
608         while ((c = getopt_long(argc, argv, "has:H:P", options, NULL)) >= 0) {
609
610                 switch (c) {
611
612                 case 'h':
613                         help();
614                         return 0;
615
616                 case ARG_VERSION:
617                         puts(PACKAGE_STRING);
618                         puts(DISTRIBUTION);
619                         puts(SYSTEMD_FEATURES);
620                         return 0;
621
622                 case 'P':
623                         arg_transport = TRANSPORT_POLKIT;
624                         break;
625
626                 case 'H':
627                         arg_transport = TRANSPORT_SSH;
628                         arg_host = optarg;
629                         break;
630
631                 case ARG_NO_CONVERT:
632                         arg_convert = false;
633                         break;
634
635                 case ARG_NO_PAGER:
636                         arg_no_pager = true;
637                         break;
638
639                 case '?':
640                         return -EINVAL;
641
642                 default:
643                         log_error("Unknown option code %c", c);
644                         return -EINVAL;
645                 }
646         }
647
648         return 1;
649 }
650
651 static int localectl_main(DBusConnection *bus, int argc, char *argv[], DBusError *error) {
652
653         static const struct {
654                 const char* verb;
655                 const enum {
656                         MORE,
657                         LESS,
658                         EQUAL
659                 } argc_cmp;
660                 const int argc;
661                 int (* const dispatch)(DBusConnection *bus, char **args, unsigned n);
662         } verbs[] = {
663                 { "status",         LESS,   1, show_status           },
664                 { "set-locale",     MORE,   2, set_locale            },
665                 { "list-locales",   EQUAL,  1, list_locales          },
666                 { "set-keymap",     MORE,   2, set_vconsole_keymap   },
667                 { "list-keymaps",   EQUAL,  1, list_vconsole_keymaps },
668                 { "set-x11-keymap", MORE,   2, set_x11_keymap        },
669         };
670
671         int left;
672         unsigned i;
673
674         assert(argc >= 0);
675         assert(argv);
676         assert(error);
677
678         left = argc - optind;
679
680         if (left <= 0)
681                 /* Special rule: no arguments means "status" */
682                 i = 0;
683         else {
684                 if (streq(argv[optind], "help")) {
685                         help();
686                         return 0;
687                 }
688
689                 for (i = 0; i < ELEMENTSOF(verbs); i++)
690                         if (streq(argv[optind], verbs[i].verb))
691                                 break;
692
693                 if (i >= ELEMENTSOF(verbs)) {
694                         log_error("Unknown operation %s", argv[optind]);
695                         return -EINVAL;
696                 }
697         }
698
699         switch (verbs[i].argc_cmp) {
700
701         case EQUAL:
702                 if (left != verbs[i].argc) {
703                         log_error("Invalid number of arguments.");
704                         return -EINVAL;
705                 }
706
707                 break;
708
709         case MORE:
710                 if (left < verbs[i].argc) {
711                         log_error("Too few arguments.");
712                         return -EINVAL;
713                 }
714
715                 break;
716
717         case LESS:
718                 if (left > verbs[i].argc) {
719                         log_error("Too many arguments.");
720                         return -EINVAL;
721                 }
722
723                 break;
724
725         default:
726                 assert_not_reached("Unknown comparison operator.");
727         }
728
729         if (!bus) {
730                 log_error("Failed to get D-Bus connection: %s", error->message);
731                 return -EIO;
732         }
733
734         return verbs[i].dispatch(bus, argv + optind, left);
735 }
736
737 int main(int argc, char *argv[]) {
738         int r, retval = EXIT_FAILURE;
739         DBusConnection *bus = NULL;
740         DBusError error;
741
742         dbus_error_init(&error);
743
744         setlocale(LC_ALL, "");
745         log_parse_environment();
746         log_open();
747
748         r = parse_argv(argc, argv);
749         if (r < 0)
750                 goto finish;
751         else if (r == 0) {
752                 retval = EXIT_SUCCESS;
753                 goto finish;
754         }
755
756         if (arg_transport == TRANSPORT_NORMAL)
757                 bus = dbus_bus_get_private(DBUS_BUS_SYSTEM, &error);
758         else if (arg_transport == TRANSPORT_POLKIT)
759                 bus_connect_system_polkit(&bus, &error);
760         else if (arg_transport == TRANSPORT_SSH)
761                 bus_connect_system_ssh(NULL, arg_host, &bus, &error);
762         else
763                 assert_not_reached("Uh, invalid transport...");
764
765         r = localectl_main(bus, argc, argv, &error);
766         retval = r < 0 ? EXIT_FAILURE : r;
767
768 finish:
769         if (bus) {
770                 dbus_connection_flush(bus);
771                 dbus_connection_close(bus);
772                 dbus_connection_unref(bus);
773         }
774
775         dbus_error_free(&error);
776         dbus_shutdown();
777
778         pager_close();
779
780         return retval;
781 }