ia64/xen-unstable

view tools/xenstore/xs_test.c @ 7329:74d56b7ff46c

Merged
author djm@kirby.fc.hp.com
date Tue Oct 11 16:57:44 2005 -0600 (2005-10-11)
parents 4e0c94871be2 f1e8d5f64105
children b3a255e88810
line source
1 /*
2 Xen Store Daemon Test tool
3 Copyright (C) 2005 Rusty Russell IBM Corporation
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 */
20 #define _GNU_SOURCE
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <sys/types.h>
24 #include <sys/wait.h>
25 #include <sys/stat.h>
26 #include <fcntl.h>
27 #include <signal.h>
28 #include <stdint.h>
29 #include <stdbool.h>
30 #include <stdlib.h>
31 #include <sys/mman.h>
32 #include <fnmatch.h>
33 #include <stdarg.h>
34 #include <string.h>
35 #include <getopt.h>
36 #include <ctype.h>
37 #include <sys/time.h>
38 #include "utils.h"
39 #include "xs_lib.h"
40 #include "list.h"
42 #define XSTEST
44 static struct xs_handle *handles[10] = { NULL };
45 static struct xs_transaction_handle *txh[10] = { NULL };
47 static unsigned int timeout_ms = 500;
48 static bool timeout_suppressed = true;
49 static bool readonly = false;
50 static bool print_input = false;
51 static unsigned int linenum = 0;
53 struct ringbuf_head
54 {
55 uint32_t write; /* Next place to write to */
56 uint32_t read; /* Next place to read from */
57 uint8_t flags;
58 char buf[0];
59 } __attribute__((packed));
61 static struct ringbuf_head *out, *in;
62 static unsigned int ringbuf_datasize;
63 static int daemon_pid;
65 /* FIXME: Mark connection as broken (close it?) when this happens. */
66 static bool check_buffer(const struct ringbuf_head *h)
67 {
68 return (h->write < ringbuf_datasize && h->read < ringbuf_datasize);
69 }
71 /* We can't fill last byte: would look like empty buffer. */
72 static void *get_output_chunk(const struct ringbuf_head *h,
73 void *buf, uint32_t *len)
74 {
75 uint32_t read_mark;
77 if (h->read == 0)
78 read_mark = ringbuf_datasize - 1;
79 else
80 read_mark = h->read - 1;
82 /* Here to the end of buffer, unless they haven't read some out. */
83 *len = ringbuf_datasize - h->write;
84 if (read_mark >= h->write)
85 *len = read_mark - h->write;
86 return buf + h->write;
87 }
89 static const void *get_input_chunk(const struct ringbuf_head *h,
90 const void *buf, uint32_t *len)
91 {
92 /* Here to the end of buffer, unless they haven't written some. */
93 *len = ringbuf_datasize - h->read;
94 if (h->write >= h->read)
95 *len = h->write - h->read;
96 return buf + h->read;
97 }
99 static int output_avail(struct ringbuf_head *out)
100 {
101 unsigned int avail;
103 get_output_chunk(out, out->buf, &avail);
104 return avail != 0;
105 }
107 static void update_output_chunk(struct ringbuf_head *h, uint32_t len)
108 {
109 h->write += len;
110 if (h->write == ringbuf_datasize)
111 h->write = 0;
112 }
114 static void update_input_chunk(struct ringbuf_head *h, uint32_t len)
115 {
116 h->read += len;
117 if (h->read == ringbuf_datasize)
118 h->read = 0;
119 }
121 /* FIXME: We spin, and we're sloppy. */
122 static bool read_all_shmem(int fd __attribute__((unused)),
123 void *data, unsigned int len)
124 {
125 unsigned int avail;
126 int was_full;
128 if (!check_buffer(in))
129 barf("Corrupt buffer");
131 was_full = !output_avail(in);
132 while (len) {
133 const void *src = get_input_chunk(in, in->buf, &avail);
134 if (avail > len)
135 avail = len;
136 memcpy(data, src, avail);
137 data += avail;
138 len -= avail;
139 update_input_chunk(in, avail);
140 }
142 /* Tell other end we read something. */
143 if (was_full)
144 kill(daemon_pid, SIGUSR2);
145 return true;
146 }
148 static bool write_all_shmem(int fd __attribute__((unused)),
149 const void *data, unsigned int len)
150 {
151 uint32_t avail;
153 if (!check_buffer(out))
154 barf("Corrupt buffer");
156 while (len) {
157 void *dst = get_output_chunk(out, out->buf, &avail);
158 if (avail > len)
159 avail = len;
160 memcpy(dst, data, avail);
161 data += avail;
162 len -= avail;
163 update_output_chunk(out, avail);
164 }
166 /* Tell other end we wrote something. */
167 kill(daemon_pid, SIGUSR2);
168 return true;
169 }
171 static bool read_all(int fd, void *data, unsigned int len);
172 static bool read_all_choice(int fd, void *data, unsigned int len)
173 {
174 if (fd == -2)
175 return read_all_shmem(fd, data, len);
176 return read_all(fd, data, len);
177 }
179 static bool write_all_choice(int fd, const void *data, unsigned int len)
180 {
181 if (fd == -2)
182 return write_all_shmem(fd, data, len);
183 return xs_write_all(fd, data, len);
184 }
186 /* We want access to internal functions. */
187 #include "xs.c"
189 static void __attribute__((noreturn)) usage(void)
190 {
191 barf("Usage:\n"
192 " xs_test [--readonly] [--no-timeout] [-x]\n"
193 "Reads commands from stdin, one per line:"
194 " dir <path>\n"
195 " read <path>\n"
196 " write <path> <value>...\n"
197 " setid <id>\n"
198 " mkdir <path>\n"
199 " rm <path>\n"
200 " getperm <path>\n"
201 " setperm <path> <id> <flags> ...\n"
202 " watch <path> <token>\n"
203 " watchnoack <path> <token>\n"
204 " waitwatch\n"
205 " unwatch <path> <token>\n"
206 " close\n"
207 " start <node>\n"
208 " abort\n"
209 " introduce <domid> <mfn> <eventchn> <path>\n"
210 " commit\n"
211 " sleep <milliseconds>\n"
212 " expect <pattern>\n"
213 " notimeout\n"
214 " readonly\n"
215 " readwrite\n"
216 " dump\n");
217 }
219 static int argpos(const char *line, unsigned int num)
220 {
221 unsigned int i, len = 0, off = 0;
223 for (i = 0; i <= num; i++) {
224 off += len;
225 off += strspn(line + off, " \t\n");
226 len = strcspn(line + off, " \t\n");
227 if (!len)
228 return off;
229 }
230 return off;
231 }
233 static char *arg(const char *line, unsigned int num)
234 {
235 static char *args[10];
236 unsigned int off, len;
238 off = argpos(line, num);
239 len = strcspn(line + off, " \t\n");
241 if (!len)
242 barf("Can't get arg %u", num);
244 free(args[num]);
245 args[num] = malloc(len + 1);
246 memcpy(args[num], line+off, len);
247 args[num][len] = '\0';
248 return args[num];
249 }
251 struct expect
252 {
253 struct list_head list;
254 char *pattern;
255 };
256 static LIST_HEAD(expects);
258 static char *command;
260 /* Trim leading and trailing whitespace */
261 static void trim(char *str)
262 {
263 while (isspace(str[0]))
264 memmove(str, str+1, strlen(str));
266 while (strlen(str) && isspace(str[strlen(str)-1]))
267 str[strlen(str)-1] = '\0';
268 }
270 static void output(const char *fmt, ...)
271 {
272 char *str;
273 struct expect *i;
274 va_list arglist;
276 va_start(arglist, fmt);
277 vasprintf(&str, fmt, arglist);
278 va_end(arglist);
280 printf("%s", str);
281 fflush(stdout);
282 trim(str);
283 list_for_each_entry(i, &expects, list) {
284 if (fnmatch(i->pattern, str, 0) == 0) {
285 list_del(&i->list);
286 free(i);
287 return;
288 }
289 }
290 barf("Unexpected output %s\n", str);
291 }
293 static void failed(int handle)
294 {
295 if (handle)
296 output("%i: %s failed: %s\n",
297 handle, command, strerror(errno));
298 else
299 output("%s failed: %s\n", command, strerror(errno));
300 }
302 static void expect(const char *line)
303 {
304 struct expect *e = malloc(sizeof(*e));
306 e->pattern = strdup(line + argpos(line, 1));
307 trim(e->pattern);
308 list_add(&e->list, &expects);
309 }
311 static void do_dir(unsigned int handle, char *path)
312 {
313 char **entries;
314 unsigned int i, num;
316 entries = xs_directory(handles[handle], txh[handle], path, &num);
317 if (!entries) {
318 failed(handle);
319 return;
320 }
322 for (i = 0; i < num; i++)
323 if (handle)
324 output("%i:%s\n", handle, entries[i]);
325 else
326 output("%s\n", entries[i]);
327 free(entries);
328 }
330 static void do_read(unsigned int handle, char *path)
331 {
332 char *value;
333 unsigned int len;
335 value = xs_read(handles[handle], txh[handle], path, &len);
336 if (!value) {
337 failed(handle);
338 return;
339 }
341 /* It's supposed to nul terminate for us. */
342 assert(value[len] == '\0');
343 if (handle)
344 output("%i:%.*s\n", handle, len, value);
345 else
346 output("%.*s\n", len, value);
347 }
349 static void do_write(unsigned int handle, char *path, char *data)
350 {
351 if (!xs_write(handles[handle], txh[handle], path, data, strlen(data)))
352 failed(handle);
353 }
355 static void do_setid(unsigned int handle, char *id)
356 {
357 if (!xs_bool(xs_debug_command(handles[handle], "setid", id,
358 strlen(id)+1)))
359 failed(handle);
360 }
362 static void do_mkdir(unsigned int handle, char *path)
363 {
364 if (!xs_mkdir(handles[handle], txh[handle], path))
365 failed(handle);
366 }
368 static void do_rm(unsigned int handle, char *path)
369 {
370 if (!xs_rm(handles[handle], txh[handle], path))
371 failed(handle);
372 }
374 static void do_getperm(unsigned int handle, char *path)
375 {
376 unsigned int i, num;
377 struct xs_permissions *perms;
379 perms = xs_get_permissions(handles[handle], txh[handle], path, &num);
380 if (!perms) {
381 failed(handle);
382 return;
383 }
385 for (i = 0; i < num; i++) {
386 char *permstring;
388 switch (perms[i].perms) {
389 case XS_PERM_NONE:
390 permstring = "NONE";
391 break;
392 case XS_PERM_WRITE:
393 permstring = "WRITE";
394 break;
395 case XS_PERM_READ:
396 permstring = "READ";
397 break;
398 case XS_PERM_READ|XS_PERM_WRITE:
399 permstring = "READ/WRITE";
400 break;
401 default:
402 barf("bad perm value %i", perms[i].perms);
403 }
405 if (handle)
406 output("%i:%i %s\n", handle, perms[i].id, permstring);
407 else
408 output("%i %s\n", perms[i].id, permstring);
409 }
410 free(perms);
411 }
413 static void do_setperm(unsigned int handle, char *path, char *line)
414 {
415 unsigned int i;
416 struct xs_permissions perms[100];
418 strtok(line, " \t\n");
419 strtok(NULL, " \t\n");
420 for (i = 0; ; i++) {
421 char *arg = strtok(NULL, " \t\n");
422 if (!arg)
423 break;
424 perms[i].id = atoi(arg);
425 arg = strtok(NULL, " \t\n");
426 if (!arg)
427 break;
428 if (streq(arg, "WRITE"))
429 perms[i].perms = XS_PERM_WRITE;
430 else if (streq(arg, "READ"))
431 perms[i].perms = XS_PERM_READ;
432 else if (streq(arg, "READ/WRITE"))
433 perms[i].perms = XS_PERM_READ|XS_PERM_WRITE;
434 else if (streq(arg, "NONE"))
435 perms[i].perms = XS_PERM_NONE;
436 else
437 barf("bad flags %s\n", arg);
438 }
440 if (!xs_set_permissions(handles[handle], txh[handle], path, perms, i))
441 failed(handle);
442 }
444 static void do_watch(unsigned int handle, const char *node, const char *token,
445 bool swallow_event)
446 {
447 if (!xs_watch(handles[handle], node, token))
448 failed(handle);
450 /* Convenient for testing... */
451 if (swallow_event) {
452 unsigned int num;
453 char **vec = xs_read_watch(handles[handle], &num);
454 if (!vec ||
455 !streq(vec[XS_WATCH_PATH], node) ||
456 !streq(vec[XS_WATCH_TOKEN], token))
457 failed(handle);
458 }
459 }
461 static void set_timeout(void)
462 {
463 struct itimerval timeout;
465 timeout.it_value.tv_sec = timeout_ms / 1000;
466 timeout.it_value.tv_usec = (timeout_ms * 1000) % 1000000;
467 timeout.it_interval.tv_sec = timeout.it_interval.tv_usec = 0;
468 setitimer(ITIMER_REAL, &timeout, NULL);
469 }
471 static void disarm_timeout(void)
472 {
473 struct itimerval timeout;
475 timeout.it_value.tv_sec = 0;
476 timeout.it_value.tv_usec = 0;
477 setitimer(ITIMER_REAL, &timeout, NULL);
478 }
480 static void do_waitwatch(unsigned int handle)
481 {
482 char **vec;
483 struct timeval tv = {.tv_sec = timeout_ms/1000,
484 .tv_usec = (timeout_ms*1000)%1000000 };
485 fd_set set;
486 unsigned int num;
488 if (xs_fileno(handles[handle]) != -2) {
489 /* Manually select here so we can time out gracefully. */
490 FD_ZERO(&set);
491 FD_SET(xs_fileno(handles[handle]), &set);
492 disarm_timeout();
493 if (select(xs_fileno(handles[handle])+1, &set,
494 NULL, NULL, &tv) == 0) {
495 errno = ETIMEDOUT;
496 failed(handle);
497 return;
498 }
499 set_timeout();
500 }
502 vec = xs_read_watch(handles[handle], &num);
503 if (!vec) {
504 failed(handle);
505 return;
506 }
508 if (handle)
509 output("%i:%s:%s\n", handle,
510 vec[XS_WATCH_PATH], vec[XS_WATCH_TOKEN]);
511 else
512 output("%s:%s\n", vec[XS_WATCH_PATH], vec[XS_WATCH_TOKEN]);
513 free(vec);
514 }
516 static void do_unwatch(unsigned int handle, const char *node, const char *token)
517 {
518 if (!xs_unwatch(handles[handle], node, token))
519 failed(handle);
520 }
522 static void do_start(unsigned int handle)
523 {
524 txh[handle] = xs_transaction_start(handles[handle]);
525 if (txh[handle] == NULL)
526 failed(handle);
527 }
529 static void do_end(unsigned int handle, bool abort)
530 {
531 if (!xs_transaction_end(handles[handle], txh[handle], abort))
532 failed(handle);
533 txh[handle] = NULL;
534 }
536 static void do_introduce(unsigned int handle,
537 const char *domid,
538 const char *mfn,
539 const char *eventchn,
540 const char *path)
541 {
542 unsigned int i;
543 int fd;
545 /* This mechanism is v. slow w. valgrind running. */
546 timeout_ms = 5000;
548 /* We poll, so ignore signal */
549 signal(SIGUSR2, SIG_IGN);
550 for (i = 0; i < ARRAY_SIZE(handles); i++)
551 if (!handles[i])
552 break;
554 fd = open("/tmp/xcmap", O_RDWR);
555 /* Set in and out pointers. */
556 out = mmap(NULL, getpagesize(), PROT_WRITE|PROT_READ, MAP_SHARED,fd,0);
557 if (out == MAP_FAILED)
558 barf_perror("Failed to map /tmp/xcmap page");
559 in = (void *)out + getpagesize() / 2;
560 close(fd);
562 /* Tell them the event channel and our PID. */
563 *(int *)((void *)out + 32) = getpid();
564 *(u16 *)((void *)out + 36) = atoi(eventchn);
566 if (!xs_introduce_domain(handles[handle], atoi(domid),
567 atol(mfn), atoi(eventchn), path)) {
568 failed(handle);
569 munmap(out, getpagesize());
570 return;
571 }
572 output("handle is %i\n", i);
574 /* Create new handle. */
575 handles[i] = new(struct xs_handle);
576 handles[i]->fd = -2;
578 /* Read in daemon pid. */
579 daemon_pid = *(int *)((void *)out + 32);
580 }
582 static void do_release(unsigned int handle, const char *domid)
583 {
584 if (!xs_release_domain(handles[handle], atoi(domid)))
585 failed(handle);
586 }
588 static int strptrcmp(const void *a, const void *b)
589 {
590 return strcmp(*(char **)a, *(char **)b);
591 }
593 static void sort_dir(char **dir, unsigned int num)
594 {
595 qsort(dir, num, sizeof(char *), strptrcmp);
596 }
598 static void dump_dir(unsigned int handle,
599 const char *node,
600 char **dir,
601 unsigned int numdirs,
602 unsigned int depth)
603 {
604 unsigned int i;
605 char spacing[depth+1];
607 memset(spacing, ' ', depth);
608 spacing[depth] = '\0';
610 sort_dir(dir, numdirs);
612 for (i = 0; i < numdirs; i++) {
613 struct xs_permissions *perms;
614 unsigned int j, numperms;
615 unsigned int len;
616 char *contents;
617 unsigned int subnum;
618 char **subdirs;
619 char subnode[strlen(node) + 1 + strlen(dir[i]) + 1];
621 sprintf(subnode, "%s/%s", node, dir[i]);
623 perms = xs_get_permissions(handles[handle], txh[handle],
624 subnode,&numperms);
625 if (!perms) {
626 failed(handle);
627 return;
628 }
630 output("%s%s: ", spacing, dir[i]);
631 for (j = 0; j < numperms; j++) {
632 char buffer[100];
633 if (!xs_perm_to_string(&perms[j], buffer))
634 barf("perm to string");
635 output("%s ", buffer);
636 }
637 free(perms);
638 output("\n");
640 /* Even directories can have contents. */
641 contents = xs_read(handles[handle], txh[handle],
642 subnode, &len);
643 if (!contents) {
644 if (errno != EISDIR)
645 failed(handle);
646 } else {
647 output(" %s(%.*s)\n", spacing, len, contents);
648 free(contents);
649 }
651 /* Every node is a directory. */
652 subdirs = xs_directory(handles[handle], txh[handle],
653 subnode, &subnum);
654 if (!subdirs) {
655 failed(handle);
656 return;
657 }
658 dump_dir(handle, subnode, subdirs, subnum, depth+1);
659 free(subdirs);
660 }
661 }
663 static void dump(int handle)
664 {
665 char **subdirs;
666 unsigned int subnum;
668 subdirs = xs_directory(handles[handle], txh[handle], "/", &subnum);
669 if (!subdirs) {
670 failed(handle);
671 return;
672 }
674 dump_dir(handle, "", subdirs, subnum, 0);
675 free(subdirs);
676 }
678 static int handle;
680 static void alarmed(int sig __attribute__((unused)))
681 {
682 if (handle) {
683 char handlename[10];
684 sprintf(handlename, "%u:", handle);
685 write(STDOUT_FILENO, handlename, strlen(handlename));
686 }
687 write(STDOUT_FILENO, command, strlen(command));
688 write(STDOUT_FILENO, " timeout\n", strlen(" timeout\n"));
689 exit(1);
690 }
692 static void do_command(unsigned int default_handle, char *line)
693 {
694 char *endp;
696 if (print_input)
697 printf("%i> %s", ++linenum, line);
699 if (strspn(line, " \n") == strlen(line))
700 return;
701 if (strstarts(line, "#"))
702 return;
704 handle = strtoul(line, &endp, 10);
705 if (endp != line)
706 memmove(line, endp+1, strlen(endp));
707 else
708 handle = default_handle;
710 command = arg(line, 0);
711 if (!handles[handle]) {
712 if (readonly)
713 handles[handle] = xs_daemon_open_readonly();
714 else
715 handles[handle] = xs_daemon_open();
716 if (!handles[handle])
717 barf_perror("Opening connection to daemon");
718 }
720 if (!timeout_suppressed)
721 set_timeout();
722 timeout_suppressed = false;
724 if (streq(command, "dir"))
725 do_dir(handle, arg(line, 1));
726 else if (streq(command, "read"))
727 do_read(handle, arg(line, 1));
728 else if (streq(command, "write"))
729 do_write(handle, arg(line, 1), arg(line, 2));
730 else if (streq(command, "setid"))
731 do_setid(handle, arg(line, 1));
732 else if (streq(command, "mkdir"))
733 do_mkdir(handle, arg(line, 1));
734 else if (streq(command, "rm"))
735 do_rm(handle, arg(line, 1));
736 else if (streq(command, "getperm"))
737 do_getperm(handle, arg(line, 1));
738 else if (streq(command, "setperm"))
739 do_setperm(handle, arg(line, 1), line);
740 else if (streq(command, "watch"))
741 do_watch(handle, arg(line, 1), arg(line, 2), true);
742 else if (streq(command, "watchnoack"))
743 do_watch(handle, arg(line, 1), arg(line, 2), false);
744 else if (streq(command, "waitwatch"))
745 do_waitwatch(handle);
746 else if (streq(command, "unwatch"))
747 do_unwatch(handle, arg(line, 1), arg(line, 2));
748 else if (streq(command, "close")) {
749 xs_daemon_close(handles[handle]);
750 handles[handle] = NULL;
751 txh[handle] = NULL;
752 } else if (streq(command, "start"))
753 do_start(handle);
754 else if (streq(command, "commit"))
755 do_end(handle, false);
756 else if (streq(command, "abort"))
757 do_end(handle, true);
758 else if (streq(command, "introduce"))
759 do_introduce(handle, arg(line, 1), arg(line, 2),
760 arg(line, 3), arg(line, 4));
761 else if (streq(command, "release"))
762 do_release(handle, arg(line, 1));
763 else if (streq(command, "dump"))
764 dump(handle);
765 else if (streq(command, "sleep")) {
766 disarm_timeout();
767 usleep(atoi(arg(line, 1)) * 1000);
768 } else if (streq(command, "expect"))
769 expect(line);
770 else if (streq(command, "notimeout"))
771 timeout_suppressed = true;
772 else if (streq(command, "readonly")) {
773 readonly = true;
774 xs_daemon_close(handles[handle]);
775 handles[handle] = NULL;
776 } else if (streq(command, "readwrite")) {
777 readonly = false;
778 xs_daemon_close(handles[handle]);
779 handles[handle] = NULL;
780 } else
781 barf("Unknown command %s", command);
782 fflush(stdout);
783 disarm_timeout();
785 /* Check expectations. */
786 if (!streq(command, "expect")) {
787 struct expect *i = list_top(&expects, struct expect, list);
789 if (i)
790 barf("Expected '%s', didn't happen\n", i->pattern);
791 }
792 }
794 static struct option options[] = { { "readonly", 0, NULL, 'r' },
795 { "no-timeout", 0, NULL, 't' },
796 { NULL, 0, NULL, 0 } };
798 int main(int argc, char *argv[])
799 {
800 int opt;
801 char line[1024];
803 while ((opt = getopt_long(argc, argv, "xrt", options, NULL)) != -1) {
804 switch (opt) {
805 case 'r':
806 readonly = true;
807 break;
808 case 't':
809 timeout_ms = 0;
810 break;
811 case 'x':
812 print_input = true;
813 break;
814 }
815 }
817 if (optind + 1 == argc) {
818 int fd = open(argv[optind], O_RDONLY);
819 if (!fd)
820 barf_perror("Opening %s", argv[optind]);
821 dup2(fd, STDIN_FILENO);
822 } else if (optind != argc)
823 usage();
826 /* The size of the ringbuffer: half a page minus head structure. */
827 ringbuf_datasize = getpagesize() / 2 - sizeof(struct ringbuf_head);
829 signal(SIGALRM, alarmed);
830 while (fgets(line, sizeof(line), stdin))
831 do_command(0, line);
833 return 0;
834 }
836 /*
837 * Local variables:
838 * c-file-style: "linux"
839 * indent-tabs-mode: t
840 * c-indent-level: 8
841 * c-basic-offset: 8
842 * tab-width: 8
843 * End:
844 */