]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/machine/machinectl.c
loginctl: suppress cgroup tree output if cgroup is empty
[thirdparty/systemd.git] / src / machine / machinectl.c
CommitLineData
1ee306e1
LP
1/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3/***
4 This file is part of systemd.
5
6 Copyright 2013 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 <dbus/dbus.h>
23#include <unistd.h>
24#include <errno.h>
25#include <string.h>
26#include <getopt.h>
27#include <pwd.h>
28#include <locale.h>
29
30#include "log.h"
31#include "util.h"
32#include "macro.h"
33#include "pager.h"
34#include "dbus-common.h"
35#include "build.h"
36#include "strv.h"
aa1936ea 37#include "unit-name.h"
1ee306e1 38#include "cgroup-show.h"
9d127096 39#include "cgroup-util.h"
1ee306e1
LP
40#include "spawn-polkit-agent.h"
41
42static char **arg_property = NULL;
43static bool arg_all = false;
44static bool arg_full = false;
45static bool arg_no_pager = false;
46static const char *arg_kill_who = NULL;
47static int arg_signal = SIGTERM;
48static enum transport {
49 TRANSPORT_NORMAL,
50 TRANSPORT_SSH,
51 TRANSPORT_POLKIT
52} arg_transport = TRANSPORT_NORMAL;
53static bool arg_ask_password = true;
54static char *arg_host = NULL;
55static char *arg_user = NULL;
56
57static void pager_open_if_enabled(void) {
58
59 /* Cache result before we open the pager */
60 if (arg_no_pager)
61 return;
62
63 pager_open(false);
64}
65
66static int list_machines(DBusConnection *bus, char **args, unsigned n) {
67 _cleanup_dbus_message_unref_ DBusMessage *reply = NULL;
68 DBusMessageIter iter, sub, sub2;
69 unsigned k = 0;
70 int r;
71
72 pager_open_if_enabled();
73
74 r = bus_method_call_with_reply (
75 bus,
76 "org.freedesktop.machine1",
77 "/org/freedesktop/machine1",
78 "org.freedesktop.machine1.Manager",
79 "ListMachines",
80 &reply,
81 NULL,
82 DBUS_TYPE_INVALID);
83 if (r)
84 return r;
85
86 if (!dbus_message_iter_init(reply, &iter) ||
87 dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY ||
88 dbus_message_iter_get_element_type(&iter) != DBUS_TYPE_STRUCT) {
89 log_error("Failed to parse reply.");
90 return -EIO;
91 }
92
93 dbus_message_iter_recurse(&iter, &sub);
94
95 if (on_tty())
96 printf("%-32s %-9s %-16s\n", "MACHINE", "CONTAINER", "SERVICE");
97
98 while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) {
99 const char *name, *class, *service, *object;
100
101 if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_STRUCT) {
102 log_error("Failed to parse reply.");
103 return -EIO;
104 }
105
106 dbus_message_iter_recurse(&sub, &sub2);
107
108 if (bus_iter_get_basic_and_next(&sub2, DBUS_TYPE_STRING, &name, true) < 0 ||
109 bus_iter_get_basic_and_next(&sub2, DBUS_TYPE_STRING, &class, true) < 0 ||
110 bus_iter_get_basic_and_next(&sub2, DBUS_TYPE_STRING, &service, true) < 0 ||
111 bus_iter_get_basic_and_next(&sub2, DBUS_TYPE_OBJECT_PATH, &object, false) < 0) {
112 log_error("Failed to parse reply.");
113 return -EIO;
114 }
115
116 printf("%-32s %-9s %-16s\n", name, class, service);
117
118 k++;
119
120 dbus_message_iter_next(&sub);
121 }
122
123 if (on_tty())
124 printf("\n%u machines listed.\n", k);
125
126 return 0;
127}
128
9d127096 129static int show_scope_cgroup(DBusConnection *bus, const char *unit, pid_t leader) {
aa1936ea
LP
130 const char *interface = "org.freedesktop.systemd1.Scope";
131 const char *property = "ControlGroup";
132 _cleanup_dbus_message_unref_ DBusMessage *reply = NULL;
133 _cleanup_free_ char *path = NULL;
134 DBusMessageIter iter, sub;
135 const char *cgroup;
136 DBusError error;
137 int r, output_flags;
138 unsigned c;
139
140 assert(bus);
141 assert(unit);
142
143 if (arg_transport == TRANSPORT_SSH)
144 return 0;
145
146 path = unit_dbus_path_from_name(unit);
147 if (!path)
148 return log_oom();
149
150 r = bus_method_call_with_reply(
151 bus,
152 "org.freedesktop.systemd1",
153 path,
154 "org.freedesktop.DBus.Properties",
155 "Get",
156 &reply,
157 &error,
158 DBUS_TYPE_STRING, &interface,
159 DBUS_TYPE_STRING, &property,
160 DBUS_TYPE_INVALID);
161 if (r < 0) {
162 log_error("Failed to query ControlGroup: %s", bus_error(&error, r));
163 dbus_error_free(&error);
164 return r;
165 }
166
167 if (!dbus_message_iter_init(reply, &iter) ||
168 dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) {
169 log_error("Failed to parse reply.");
170 return -EINVAL;
171 }
172
173 dbus_message_iter_recurse(&iter, &sub);
174 if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_STRING) {
175 log_error("Failed to parse reply.");
176 return -EINVAL;
177 }
178
179 dbus_message_iter_get_basic(&sub, &cgroup);
180
9d127096
LP
181 if (isempty(cgroup))
182 return 0;
183
184 if (cg_is_empty_recursive(SYSTEMD_CGROUP_CONTROLLER, cgroup, false) != 0 && leader <= 0)
185 return 0;
186
aa1936ea
LP
187 output_flags =
188 arg_all * OUTPUT_SHOW_ALL |
189 arg_full * OUTPUT_FULL_WIDTH;
190
191 c = columns();
192 if (c > 18)
193 c -= 18;
194 else
195 c = 0;
196
9d127096 197 show_cgroup_and_extra(SYSTEMD_CGROUP_CONTROLLER, cgroup, "\t\t ", c, false, &leader, leader > 0, output_flags);
aa1936ea
LP
198 return 0;
199}
200
1ee306e1
LP
201typedef struct MachineStatusInfo {
202 const char *name;
203 sd_id128_t id;
1ee306e1
LP
204 const char *class;
205 const char *service;
aa1936ea 206 const char *scope;
1ee306e1
LP
207 const char *root_directory;
208 pid_t leader;
209 usec_t timestamp;
210} MachineStatusInfo;
211
aa1936ea 212static void print_machine_status_info(DBusConnection *bus, MachineStatusInfo *i) {
1ee306e1
LP
213 char since1[FORMAT_TIMESTAMP_RELATIVE_MAX], *s1;
214 char since2[FORMAT_TIMESTAMP_MAX], *s2;
215 assert(i);
216
217 fputs(strna(i->name), stdout);
218
219 if (!sd_id128_equal(i->id, SD_ID128_NULL))
220 printf("(" SD_ID128_FORMAT_STR ")\n", SD_ID128_FORMAT_VAL(i->id));
221 else
222 putchar('\n');
223
224 s1 = format_timestamp_relative(since1, sizeof(since1), i->timestamp);
225 s2 = format_timestamp(since2, sizeof(since2), i->timestamp);
226
227 if (s1)
228 printf("\t Since: %s; %s\n", s2, s1);
229 else if (s2)
230 printf("\t Since: %s\n", s2);
231
232 if (i->leader > 0) {
233 _cleanup_free_ char *t = NULL;
234
235 printf("\t Leader: %u", (unsigned) i->leader);
236
237 get_process_comm(i->leader, &t);
238 if (t)
239 printf(" (%s)", t);
240
241 putchar('\n');
242 }
243
244 if (i->service) {
245 printf("\t Service: %s", i->service);
246
247 if (i->class)
248 printf("; class %s", i->class);
249
250 putchar('\n');
251 } else if (i->class)
252 printf("\t Class: %s\n", i->class);
253
1ee306e1
LP
254 if (i->root_directory)
255 printf("\t Root: %s\n", i->root_directory);
256
aa1936ea
LP
257 if (i->scope) {
258 printf("\t Unit: %s\n", i->scope);
9d127096 259 show_scope_cgroup(bus, i->scope, i->leader);
1ee306e1
LP
260 }
261}
262
263static int status_property_machine(const char *name, DBusMessageIter *iter, MachineStatusInfo *i) {
264 assert(name);
265 assert(iter);
266 assert(i);
267
268 switch (dbus_message_iter_get_arg_type(iter)) {
269
270 case DBUS_TYPE_STRING: {
271 const char *s;
272
273 dbus_message_iter_get_basic(iter, &s);
274
275 if (!isempty(s)) {
276 if (streq(name, "Name"))
277 i->name = s;
1ee306e1
LP
278 else if (streq(name, "Class"))
279 i->class = s;
280 else if (streq(name, "Service"))
281 i->service = s;
aa1936ea
LP
282 else if (streq(name, "Scope"))
283 i->scope = s;
1ee306e1
LP
284 else if (streq(name, "RootDirectory"))
285 i->root_directory = s;
286 }
287 break;
288 }
289
290 case DBUS_TYPE_UINT32: {
291 uint32_t u;
292
293 dbus_message_iter_get_basic(iter, &u);
294
295 if (streq(name, "Leader"))
296 i->leader = (pid_t) u;
297
298 break;
299 }
300
301 case DBUS_TYPE_UINT64: {
302 uint64_t u;
303
304 dbus_message_iter_get_basic(iter, &u);
305
306 if (streq(name, "Timestamp"))
307 i->timestamp = (usec_t) u;
308
309 break;
310 }
311
312 case DBUS_TYPE_ARRAY: {
313 DBusMessageIter sub;
314
315 dbus_message_iter_recurse(iter, &sub);
316
317 if (dbus_message_iter_get_arg_type(&sub) == DBUS_TYPE_BYTE && streq(name, "Id")) {
318 void *v;
319 int n;
320
321 dbus_message_iter_get_fixed_array(&sub, &v, &n);
322 if (n == 0)
323 i->id = SD_ID128_NULL;
324 else if (n == 16)
325 memcpy(&i->id, v, n);
326 }
327
328 break;
329 }
330 }
331
332 return 0;
333}
334
335static int print_property(const char *name, DBusMessageIter *iter) {
336 assert(name);
337 assert(iter);
338
339 if (arg_property && !strv_find(arg_property, name))
340 return 0;
341
342 if (generic_print_property(name, iter, arg_all) > 0)
343 return 0;
344
345 if (arg_all)
346 printf("%s=[unprintable]\n", name);
347
348 return 0;
349}
350
351static int show_one(const char *verb, DBusConnection *bus, const char *path, bool show_properties, bool *new_line) {
352 _cleanup_dbus_message_unref_ DBusMessage *reply = NULL;
353 const char *interface = "";
354 int r;
355 DBusMessageIter iter, sub, sub2, sub3;
356 MachineStatusInfo machine_info = {};
357
358 assert(path);
359 assert(new_line);
360
361 r = bus_method_call_with_reply(
362 bus,
363 "org.freedesktop.machine1",
364 path,
365 "org.freedesktop.DBus.Properties",
366 "GetAll",
367 &reply,
368 NULL,
369 DBUS_TYPE_STRING, &interface,
370 DBUS_TYPE_INVALID);
371 if (r < 0)
372 goto finish;
373
374 if (!dbus_message_iter_init(reply, &iter) ||
375 dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY ||
376 dbus_message_iter_get_element_type(&iter) != DBUS_TYPE_DICT_ENTRY) {
377 log_error("Failed to parse reply.");
378 r = -EIO;
379 goto finish;
380 }
381
382 dbus_message_iter_recurse(&iter, &sub);
383
384 if (*new_line)
385 printf("\n");
386
387 *new_line = true;
388
389 while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) {
390 const char *name;
391
392 if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_DICT_ENTRY) {
393 log_error("Failed to parse reply.");
394 r = -EIO;
395 goto finish;
396 }
397
398 dbus_message_iter_recurse(&sub, &sub2);
399
400 if (bus_iter_get_basic_and_next(&sub2, DBUS_TYPE_STRING, &name, true) < 0) {
401 log_error("Failed to parse reply.");
402 r = -EIO;
403 goto finish;
404 }
405
406 if (dbus_message_iter_get_arg_type(&sub2) != DBUS_TYPE_VARIANT) {
407 log_error("Failed to parse reply.");
408 r = -EIO;
409 goto finish;
410 }
411
412 dbus_message_iter_recurse(&sub2, &sub3);
413
414 if (show_properties)
415 r = print_property(name, &sub3);
416 else
417 r = status_property_machine(name, &sub3, &machine_info);
418
419 if (r < 0) {
420 log_error("Failed to parse reply.");
421 goto finish;
422 }
423
424 dbus_message_iter_next(&sub);
425 }
426
427 if (!show_properties)
aa1936ea 428 print_machine_status_info(bus, &machine_info);
1ee306e1
LP
429
430 r = 0;
431
432finish:
433
434 return r;
435}
436
437static int show(DBusConnection *bus, char **args, unsigned n) {
438 _cleanup_dbus_message_unref_ DBusMessage *reply = NULL;
439 int r, ret = 0;
440 DBusError error;
441 unsigned i;
442 bool show_properties, new_line = false;
443
444 assert(bus);
445 assert(args);
446
447 dbus_error_init(&error);
448
449 show_properties = !strstr(args[0], "status");
450
451 pager_open_if_enabled();
452
453 if (show_properties && n <= 1) {
454 /* If not argument is specified inspect the manager
455 * itself */
456
457 ret = show_one(args[0], bus, "/org/freedesktop/machine1", show_properties, &new_line);
458 goto finish;
459 }
460
461 for (i = 1; i < n; i++) {
462 const char *path = NULL;
463
464 ret = bus_method_call_with_reply(
465 bus,
466 "org.freedesktop.machine1",
467 "/org/freedesktop/machine1",
468 "org.freedesktop.machine1.Manager",
469 "GetMachine",
470 &reply,
471 NULL,
472 DBUS_TYPE_STRING, &args[i],
473 DBUS_TYPE_INVALID);
474 if (ret < 0)
475 goto finish;
476
477 if (!dbus_message_get_args(reply, &error,
478 DBUS_TYPE_OBJECT_PATH, &path,
479 DBUS_TYPE_INVALID)) {
480 log_error("Failed to parse reply: %s", bus_error_message(&error));
481 ret = -EIO;
482 goto finish;
483 }
484
485 r = show_one(args[0], bus, path, show_properties, &new_line);
486 if (r != 0)
487 ret = r;
488 }
489
490finish:
491 dbus_error_free(&error);
492
493 return ret;
494}
495
496static int kill_machine(DBusConnection *bus, char **args, unsigned n) {
497 unsigned i;
498
499 assert(args);
500
501 if (!arg_kill_who)
502 arg_kill_who = "all";
503
504 for (i = 1; i < n; i++) {
505 int r;
506
507 r = bus_method_call_with_reply (
508 bus,
509 "org.freedesktop.machine1",
510 "/org/freedesktop/machine1",
511 "org.freedesktop.machine1.Manager",
512 "KillMachine",
513 NULL,
514 NULL,
515 DBUS_TYPE_STRING, &args[i],
516 DBUS_TYPE_STRING, &arg_kill_who,
517 DBUS_TYPE_INT32, &arg_signal,
518 DBUS_TYPE_INVALID);
519 if (r)
520 return r;
521 }
522
523 return 0;
524}
525
526static int terminate_machine(DBusConnection *bus, char **args, unsigned n) {
527 unsigned i;
528
529 assert(args);
530
531 for (i = 1; i < n; i++) {
532 int r;
533
534 r = bus_method_call_with_reply (
535 bus,
536 "org.freedesktop.machine1",
537 "/org/freedesktop/machine1",
538 "org.freedesktop.machine1.Manager",
539 "TerminateMachine",
540 NULL,
541 NULL,
542 DBUS_TYPE_STRING, &args[i],
543 DBUS_TYPE_INVALID);
544 if (r)
545 return r;
546 }
547
548 return 0;
549}
550
551static int help(void) {
552
553 printf("%s [OPTIONS...] {COMMAND} ...\n\n"
554 "Send control commands to or query the virtual machine and container registration manager.\n\n"
555 " -h --help Show this help\n"
556 " --version Show package version\n"
557 " -p --property=NAME Show only properties by this name\n"
558 " -a --all Show all properties, including empty ones\n"
559 " --kill-who=WHO Who to send signal to\n"
560 " -l --full Do not ellipsize output\n"
561 " -s --signal=SIGNAL Which signal to send\n"
562 " --no-ask-password Don't prompt for password\n"
563 " -H --host=[USER@]HOST Show information for remote host\n"
564 " -P --privileged Acquire privileges before execution\n"
565 " --no-pager Do not pipe output into a pager\n\n"
566 "Commands:\n"
567 " list List running VMs and containers\n"
568 " status [NAME...] Show VM/container status\n"
19887cd0 569 " show [NAME...] Show properties of one or more VMs/containers\n"
1ee306e1
LP
570 " terminate [NAME...] Terminate one or more VMs/containers\n"
571 " kill [NAME...] Send signal to processes of a VM/container\n",
572 program_invocation_short_name);
573
574 return 0;
575}
576
577static int parse_argv(int argc, char *argv[]) {
578
579 enum {
580 ARG_VERSION = 0x100,
581 ARG_NO_PAGER,
582 ARG_KILL_WHO,
583 ARG_NO_ASK_PASSWORD,
584 };
585
586 static const struct option options[] = {
587 { "help", no_argument, NULL, 'h' },
588 { "version", no_argument, NULL, ARG_VERSION },
589 { "property", required_argument, NULL, 'p' },
590 { "all", no_argument, NULL, 'a' },
591 { "full", no_argument, NULL, 'l' },
592 { "no-pager", no_argument, NULL, ARG_NO_PAGER },
593 { "kill-who", required_argument, NULL, ARG_KILL_WHO },
594 { "signal", required_argument, NULL, 's' },
595 { "host", required_argument, NULL, 'H' },
596 { "privileged", no_argument, NULL, 'P' },
597 { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD },
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, "hp:als: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(SYSTEMD_FEATURES);
617 return 0;
618
619 case 'p': {
620 char **l;
621
622 l = strv_append(arg_property, optarg);
623 if (!l)
624 return -ENOMEM;
625
626 strv_free(arg_property);
627 arg_property = l;
628
629 /* If the user asked for a particular
630 * property, show it to him, even if it is
631 * empty. */
632 arg_all = true;
633 break;
634 }
635
636 case 'a':
637 arg_all = true;
638 break;
639
640 case 'l':
641 arg_full = true;
642 break;
643
644 case ARG_NO_PAGER:
645 arg_no_pager = true;
646 break;
647
648 case ARG_NO_ASK_PASSWORD:
649 arg_ask_password = false;
650 break;
651
652 case ARG_KILL_WHO:
653 arg_kill_who = optarg;
654 break;
655
656 case 's':
657 arg_signal = signal_from_string_try_harder(optarg);
658 if (arg_signal < 0) {
659 log_error("Failed to parse signal string %s.", optarg);
660 return -EINVAL;
661 }
662 break;
663
664 case 'P':
665 arg_transport = TRANSPORT_POLKIT;
666 break;
667
668 case 'H':
669 arg_transport = TRANSPORT_SSH;
670 parse_user_at_host(optarg, &arg_user, &arg_host);
671 break;
672
673 case '?':
674 return -EINVAL;
675
676 default:
677 log_error("Unknown option code %c", c);
678 return -EINVAL;
679 }
680 }
681
682 return 1;
683}
684
685static int machinectl_main(DBusConnection *bus, int argc, char *argv[], DBusError *error) {
686
687 static const struct {
688 const char* verb;
689 const enum {
690 MORE,
691 LESS,
692 EQUAL
693 } argc_cmp;
694 const int argc;
695 int (* const dispatch)(DBusConnection *bus, char **args, unsigned n);
696 } verbs[] = {
697 { "list", LESS, 1, list_machines },
698 { "status", MORE, 2, show },
699 { "show", MORE, 1, show },
700 { "terminate", MORE, 2, terminate_machine },
701 { "kill", MORE, 2, kill_machine },
702 };
703
704 int left;
705 unsigned i;
706
707 assert(argc >= 0);
708 assert(argv);
709 assert(error);
710
711 left = argc - optind;
712
713 if (left <= 0)
714 /* Special rule: no arguments means "list-sessions" */
715 i = 0;
716 else {
717 if (streq(argv[optind], "help")) {
718 help();
719 return 0;
720 }
721
722 for (i = 0; i < ELEMENTSOF(verbs); i++)
723 if (streq(argv[optind], verbs[i].verb))
724 break;
725
726 if (i >= ELEMENTSOF(verbs)) {
727 log_error("Unknown operation %s", argv[optind]);
728 return -EINVAL;
729 }
730 }
731
732 switch (verbs[i].argc_cmp) {
733
734 case EQUAL:
735 if (left != verbs[i].argc) {
736 log_error("Invalid number of arguments.");
737 return -EINVAL;
738 }
739
740 break;
741
742 case MORE:
743 if (left < verbs[i].argc) {
744 log_error("Too few arguments.");
745 return -EINVAL;
746 }
747
748 break;
749
750 case LESS:
751 if (left > verbs[i].argc) {
752 log_error("Too many arguments.");
753 return -EINVAL;
754 }
755
756 break;
757
758 default:
759 assert_not_reached("Unknown comparison operator.");
760 }
761
762 if (!bus) {
763 log_error("Failed to get D-Bus connection: %s", error->message);
764 return -EIO;
765 }
766
767 return verbs[i].dispatch(bus, argv + optind, left);
768}
769
770int main(int argc, char*argv[]) {
771 int r, retval = EXIT_FAILURE;
772 DBusConnection *bus = NULL;
773 DBusError error;
774
775 dbus_error_init(&error);
776
777 setlocale(LC_ALL, "");
778 log_parse_environment();
779 log_open();
780
781 r = parse_argv(argc, argv);
782 if (r < 0)
783 goto finish;
784 else if (r == 0) {
785 retval = EXIT_SUCCESS;
786 goto finish;
787 }
788
789 if (arg_transport == TRANSPORT_NORMAL)
790 bus = dbus_bus_get_private(DBUS_BUS_SYSTEM, &error);
791 else if (arg_transport == TRANSPORT_POLKIT)
792 bus_connect_system_polkit(&bus, &error);
793 else if (arg_transport == TRANSPORT_SSH)
794 bus_connect_system_ssh(NULL, arg_host, &bus, &error);
795 else
796 assert_not_reached("Uh, invalid transport...");
797
798 r = machinectl_main(bus, argc, argv, &error);
799 retval = r < 0 ? EXIT_FAILURE : r;
800
801finish:
802 if (bus) {
803 dbus_connection_flush(bus);
804 dbus_connection_close(bus);
805 dbus_connection_unref(bus);
806 }
807
808 dbus_error_free(&error);
809 dbus_shutdown();
810
811 strv_free(arg_property);
812
813 pager_close();
814
815 return retval;
816}