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