source: MondoRescue/trunk/mondo/src/common/libmondo-fork.c@ 1074

Last change on this file since 1074 was 900, checked in by Bruno Cornec, 18 years ago

Huge patch to introduce low level functions that will bw used everywhere (mr_free, mr_asprintf, ...)
Nearly linking now due to that.

  • Property svn:keywords set to Id
File size: 18.4 KB
Line 
1/* libmondo-fork.c - subroutines for handling forking/pthreads/etc.
2 * $Id: libmondo-fork.c 900 2006-10-24 06:49:18Z bruno $
3 */
4
5#include "my-stuff.h"
6#include "mondostructures.h"
7#include "libmondo-fork.h"
8#include "libmondo-string-EXT.h"
9#include "newt-specific-EXT.h"
10#include "libmondo-files-EXT.h"
11#include "libmondo-tools-EXT.h"
12#include "mr_mem.h"
13
14extern bool g_text_mode;
15pid_t g_buffer_pid = 0;
16
17
18/**
19 * Call a program and retrieve its last line of output.
20 * @param call The program to run.
21 * @return The last line of its output.
22 * @note The returned value should be freed by caller
23 */
24char *call_program_and_get_last_line_of_output(char *call)
25{
26 char *result = NULL;
27 FILE *fin = NULL;
28 size_t n = 0;
29
30 assert_string_is_neither_NULL_nor_zerolength(call);
31
32 if ((fin = popen(call, "r"))) {
33 for (mr_getline(&result, &n, fin); !feof(fin); mr_getline(&result, &n, fin));
34 paranoid_pclose(fin);
35 } else {
36 log_OS_error("Unable to popen call");
37 }
38 return(result);
39}
40
41#define MONDO_POPMSG _("Your PC will not retract the CD tray automatically. Please call mondoarchive with the -m (manual CD tray) flag.")
42
43/**
44 * Call mkisofs to create an ISO image.
45 * @param bkpinfo The backup information structure. Fields used:
46 * - @c bkpinfo->manual_cd_tray
47 * - @c bkpinfo->backup_media_type
48 * - @c bkpinfo->please_dont_eject_when_restoring
49 * @param basic_call The call to mkisofs. May contain tokens that will be resolved to actual data. The tokens are:
50 * - @c _ISO_ will become the ISO file (@p isofile)
51 * - @c _CD#_ becomes the CD number (@p cd_no)
52 * - @c _ERR_ becomes the logfile (@p g_logfile)
53 * @param isofile Replaces @c _ISO_ in @p basic_call. Should probably be the ISO image to create (-o parameter to mkisofs).
54 * @param cd_no Replaces @c _CD#_ in @p basic_call. Should probably be the CD number.
55 * @param logstub Unused.
56 * @param what_i_am_doing The action taking place (e.g. "Making ISO #1"). Used as the title of the progress dialog.
57 * @return Exit code of @c mkisofs (0 is success, anything else indicates failure).
58 * @bug @p logstub is unused.
59 */
60int
61eval_call_to_make_ISO(struct s_bkpinfo *bkpinfo,
62 char *basic_call, char *isofile,
63 int cd_no, char *logstub, char *what_i_am_doing)
64{
65
66 /*@ int's *** */
67 int retval = 0;
68
69
70 /*@ buffers *** */
71 char *midway_call, *ultimate_call, *tmp, *command,
72 *cd_number_str;
73 char *p;
74
75/*@*********** End Variables ***************************************/
76
77 log_msg(3, "Starting");
78 assert(bkpinfo != NULL);
79 assert_string_is_neither_NULL_nor_zerolength(basic_call);
80 assert_string_is_neither_NULL_nor_zerolength(isofile);
81 assert_string_is_neither_NULL_nor_zerolength(logstub);
82 if (!(midway_call = malloc(1200))) {
83 fatal_error("Cannot malloc midway_call");
84 }
85 if (!(ultimate_call = malloc(1200))) {
86 fatal_error("Cannot malloc ultimate_call");
87 }
88 if (!(tmp = malloc(1200))) {
89 fatal_error("Cannot malloc tmp");
90 }
91
92 mr_asprintf(&cd_number_str, "%d", cd_no);
93 resolve_naff_tokens(midway_call, basic_call, isofile, "_ISO_");
94 resolve_naff_tokens(tmp, midway_call, cd_number_str, "_CD#_");
95 mr_free(cd_number_str);
96
97 resolve_naff_tokens(ultimate_call, tmp, MONDO_LOGFILE, "_ERR_");
98 log_msg(4, "basic call = '%s'", basic_call);
99 log_msg(4, "midway_call = '%s'", midway_call);
100 log_msg(4, "tmp = '%s'", tmp);
101 log_msg(4, "ultimate call = '%s'", ultimate_call);
102 mr_asprintf(&command, "%s >> %s", ultimate_call, MONDO_LOGFILE);
103
104 log_to_screen
105 (_("Please be patient. Do not be alarmed by on-screen inactivity."));
106 log_msg(4, "Calling open_evalcall_form() with what_i_am_doing='%s'",
107 what_i_am_doing);
108 if (bkpinfo->manual_cd_tray) {
109 /* Find everything after a 2>> and remove it */
110 p = strstr(command, "2>>");
111 if (p) {
112 for (; *p != ' ' && *p != '\0'; p++) {
113 *p = ' ';
114 }
115 }
116#ifndef _XWIN
117 if (!g_text_mode) {
118 newtSuspend();
119 }
120#endif
121 log_msg(1, "command = '%s'", command);
122 retval += system(command);
123 if (!g_text_mode) {
124 newtResume();
125 }
126 if (retval) {
127 log_msg(2, "Basic call '%s' returned an error.", basic_call);
128 popup_and_OK(_("Press ENTER to continue."));
129 popup_and_OK
130 (_("mkisofs and/or cdrecord returned an error. CD was not created"));
131 }
132 }
133 /* if text mode then do the above & RETURN; if not text mode, do this... */
134 else {
135 log_msg(3, "command = '%s'", command);
136// yes_this_is_a_goto:
137 retval =
138 run_external_binary_with_percentage_indicator_NEW
139 (what_i_am_doing, command);
140 }
141 mr_free(command);
142
143 mr_free(midway_call);
144 mr_free(ultimate_call);
145 mr_free(tmp);
146 return (retval);
147}
148
149
150/**
151 * Run a program and log its output (stdout and stderr) to the logfile.
152 * @param program The program to run. Passed to the shell, so you can use pipes etc.
153 * @param debug_level If @p g_loglevel is higher than this, do not log the output.
154 * @return The exit code of @p program (depends on the command, but 0 almost always indicates success).
155 */
156int run_program_and_log_output(char *program, int debug_level)
157{
158 /*@ buffer ****************************************************** */
159 char *callstr = NULL;
160 char *incoming = NULL;
161
162 /*@ int ********************************************************* */
163 int res = 0;
164 size_t n = 0;
165 bool log_if_failure = FALSE;
166 bool log_if_success = FALSE;
167
168 /*@ pointers *************************************************** */
169 FILE *fin;
170 char *p;
171
172 /*@ end vars *************************************************** */
173
174 assert(program != NULL);
175 if (!program[0]) {
176 log_msg(2, "Warning - asked to run zerolength program");
177 return (1);
178 }
179
180 if (debug_level <= g_loglevel) {
181 log_if_success = TRUE;
182 log_if_failure = TRUE;
183 }
184 mr_asprintf(&callstr,
185 "%s > /tmp/mondo-run-prog-thing.tmp 2> /tmp/mondo-run-prog-thing.err",
186 program);
187 while ((p = strchr(callstr, '\r'))) {
188 *p = ' ';
189 }
190 while ((p = strchr(callstr, '\n'))) {
191 *p = ' ';
192 } /* single '=' is intentional */
193
194
195 res = system(callstr);
196 if (((res == 0) && log_if_success) || ((res != 0) && log_if_failure)) {
197 log_msg(0, "running: %s", callstr);
198 log_msg(0,
199 "--------------------------------start of output-----------------------------");
200 }
201 mr_free(callstr);
202
203 if (log_if_failure
204 &&
205 system
206 ("cat /tmp/mondo-run-prog-thing.err >> /tmp/mondo-run-prog-thing.tmp 2> /dev/null"))
207 {
208 log_OS_error("Command failed");
209 }
210 unlink("/tmp/mondo-run-prog-thing.err");
211 fin = fopen("/tmp/mondo-run-prog-thing.tmp", "r");
212 if (fin) {
213 for (mr_getline(&incoming, &n, fin); !feof(fin);
214 mr_getline(&incoming, &n, fin)) {
215 /* patch by Heiko Schlittermann */
216 p = incoming;
217 while (p && *p) {
218 if ((p = strchr(p, '%'))) {
219 memmove(p, p + 1, strlen(p) + 1);
220 p += 2;
221 }
222 }
223 /* end of patch */
224 strip_spaces(incoming);
225 if ((res == 0 && log_if_success)
226 || (res != 0 && log_if_failure)) {
227 log_msg(0, incoming);
228 }
229 }
230 mr_free(incoming);
231 paranoid_fclose(fin);
232 }
233 unlink("/tmp/mondo-run-prog-thing.tmp");
234 if ((res == 0 && log_if_success) || (res != 0 && log_if_failure)) {
235 log_msg(0,
236 "--------------------------------end of output------------------------------");
237 if (res) {
238 log_msg(0, "...ran with res=%d", res);
239 } else {
240 log_msg(0, "...ran just fine. :-)");
241 }
242 }
243// else
244// { log_msg (0, "-------------------------------ran w/ res=%d------------------------------", res); }
245 return (res);
246}
247
248
249/**
250 * Run a program and log its output to the screen.
251 * @param basic_call The program to run.
252 * @param what_i_am_doing The title of the evalcall form.
253 * @return The return value of the command (varies, but 0 almost always means success).
254 * @see run_program_and_log_output
255 * @see log_to_screen
256 */
257int run_program_and_log_to_screen(char *basic_call, char *what_i_am_doing)
258{
259 /*@ int ******************************************************** */
260 int retval = 0;
261 int res = 0;
262 int i;
263
264 /*@ pointers **************************************************** */
265 FILE *fin;
266
267 /*@ buffers **************************************************** */
268 char *tmp;
269 char *command;
270 char *lockfile;
271
272 /*@ end vars *************************************************** */
273
274 assert_string_is_neither_NULL_nor_zerolength(basic_call);
275
276 mr_asprintf(&lockfile, "/tmp/mojo-jojo.blah.XXXXXX");
277 mkstemp(lockfile);
278 mr_asprintf(&command,
279 "echo hi > %s ; %s >> %s 2>> %s; res=$?; sleep 1; rm -f %s; exit $res",
280 lockfile, basic_call, MONDO_LOGFILE, MONDO_LOGFILE, lockfile);
281 open_evalcall_form(what_i_am_doing);
282 mr_asprintf(&tmp, "Executing %s", basic_call);
283 log_msg(2, tmp);
284 mr_free(tmp);
285
286 if (!(fin = popen(command, "r"))) {
287 log_OS_error("Unable to popen-in command");
288 mr_asprintf(&tmp, _("Failed utterly to call '%s'"), command);
289 log_to_screen(tmp);
290 mr_free(tmp);
291 mr_free(lockfile);
292 mr_free(command);
293 return (1);
294 }
295 mr_free(command);
296
297 if (!does_file_exist(lockfile)) {
298 log_to_screen(_("Waiting for external binary to start"));
299 for (i = 0; i < 60 && !does_file_exist(lockfile); sleep(1), i++) {
300 log_msg(3, "Waiting for lockfile %s to exist", lockfile);
301 }
302 }
303 /* This works on Newt, and it gives quicker updates. */
304 for (; does_file_exist(lockfile); sleep(1)) {
305 log_file_end_to_screen(MONDO_LOGFILE, "");
306 update_evalcall_form(1);
307 }
308 /* Evaluate the status returned by pclose to get the exit code of the called program. */
309 errno = 0;
310 res = pclose(fin);
311 /* Log actual pclose errors. */
312 if (errno)
313 log_msg(5, "pclose err: %d", errno);
314 /* Check if we have a valid status. If we do, extract the called program's exit code. */
315 /* If we don't, highlight this fact by returning -1. */
316 if (WIFEXITED(res)) {
317 retval = WEXITSTATUS(res);
318 } else {
319 retval = -1;
320 }
321 close_evalcall_form();
322 unlink(lockfile);
323 mr_free(lockfile);
324
325 return (retval);
326}
327
328
329/**
330 * Apparently used. @bug This has a purpose, but what?
331 */
332#define PIMP_START_SZ "STARTSTARTSTART9ff3kff9a82gv34r7fghbkaBBC2T231hc81h42vws8"
333#define PIMP_END_SZ "ENDENDEND0xBBC10xBBC2T231hc81h42vws89ff3kff9a82gv34r7fghbka"
334
335
336
337
338int copy_from_src_to_dest(FILE * f_orig, FILE * f_archived, char direction)
339{
340// if dir=='w' then copy from orig to archived
341// if dir=='r' then copy from archived to orig
342 char *tmp = NULL;
343 char *buf = NULL;
344 long int bytes_to_be_read, bytes_read_in, bytes_written_out =
345 0, bufcap, subsliceno = 0;
346 int retval = 0;
347 FILE *fin = NULL;
348 FILE *fout = NULL;
349 FILE *ftmp = NULL;
350
351 log_msg(5, "Opening.");
352 bufcap = 256L * 1024L;
353 if (!(buf = malloc(bufcap))) {
354 fatal_error("Failed to malloc() buf");
355 }
356
357 if (direction == 'w') {
358 fin = f_orig;
359 fout = f_archived;
360 mr_asprintf(&tmp, "%-64s", PIMP_START_SZ);
361 if (fwrite(tmp, 1, 64, fout) != 64) {
362 fatal_error("Can't write the introductory block");
363 }
364 mr_free(tmp);
365
366 while (1) {
367 bytes_to_be_read = bytes_read_in = fread(buf, 1, bufcap, fin);
368 if (bytes_read_in == 0) {
369 break;
370 }
371 mr_asprintf(&tmp, "%-64ld", bytes_read_in);
372 if (fwrite(tmp, 1, 64, fout) != 64) {
373 fatal_error("Cannot write introductory block");
374 }
375 mr_free(tmp);
376
377 log_msg(7,
378 "subslice #%ld --- I have read %ld of %ld bytes in from f_orig",
379 subsliceno, bytes_read_in, bytes_to_be_read);
380 bytes_written_out += fwrite(buf, 1, bytes_read_in, fout);
381 mr_asprintf(&tmp, "%-64ld", subsliceno);
382 if (fwrite(tmp, 1, 64, fout) != 64) {
383 fatal_error("Cannot write post-thingy block");
384 }
385 mr_free(tmp);
386
387 log_msg(7, "Subslice #%d written OK", subsliceno);
388 subsliceno++;
389 }
390 mr_asprintf(&tmp, "%-64ld", 0L);
391 if (fwrite(tmp, 1, 64L, fout) != 64L) {
392 fatal_error("Cannot write final introductory block");
393 }
394 } else {
395 fin = f_archived;
396 fout = f_orig;
397 if (!(tmp = malloc(64L))) {
398 fatal_error("Failed to malloc() tmp");
399 }
400 if (fread(tmp, 1, 64L, fin) != 64L) {
401 fatal_error("Cannot read the introductory block");
402 }
403 log_msg(5, "tmp is %s", tmp);
404 if (!strstr(tmp, PIMP_START_SZ)) {
405 fatal_error("Can't find intro blk");
406 }
407 if (fread(tmp, 1, 64L, fin) != 64L) {
408 fatal_error("Cannot read introductory blk");
409 }
410 bytes_to_be_read = atol(tmp);
411 while (bytes_to_be_read > 0) {
412 log_msg(7, "subslice#%ld, bytes=%ld", subsliceno,
413 bytes_to_be_read);
414 bytes_read_in = fread(buf, 1, bytes_to_be_read, fin);
415 if (bytes_read_in != bytes_to_be_read) {
416 fatal_error
417 ("Danger, WIll Robinson. Failed to read whole subvol from archives.");
418 }
419 bytes_written_out += fwrite(buf, 1, bytes_read_in, fout);
420 if (fread(tmp, 1, 64L, fin) != 64L) {
421 fatal_error("Cannot read post-thingy block");
422 }
423 if (atol(tmp) != subsliceno) {
424 log_msg(1, "Wanted subslice %ld but got %ld ('%s')",
425 subsliceno, atol(tmp), tmp);
426 }
427 log_msg(7, "Subslice #%ld read OK", subsliceno);
428 subsliceno++;
429 if (fread(tmp, 1, 64L, fin) != 64L) {
430 fatal_error("Cannot read introductory block");
431 }
432 bytes_to_be_read = atol(tmp);
433 }
434 }
435
436// log_msg(4, "Written %ld of %ld bytes", bytes_written_out, bytes_read_in);
437
438 if (direction == 'w') {
439 mr_free(tmp);
440 mr_asprintf(&tmp, "%-64s", PIMP_END_SZ);
441 if (fwrite(tmp, 1, 64L, fout) != 64L) {
442 fatal_error("Can't write the final block");
443 }
444 mr_free(tmp);
445 } else {
446 log_msg(1, "tmpA is %s", tmp);
447 if (!strstr(tmp, PIMP_END_SZ)) {
448 if (fread(tmp, 1, 64L, fin) != 64L) {
449 fatal_error("Can't read the final block");
450 }
451 log_msg(5, "tmpB is %s", tmp);
452 if (!strstr(tmp, PIMP_END_SZ)) {
453 ftmp = fopen("/tmp/out.leftover", "w");
454 bytes_read_in = fread(tmp, 1, 64L, fin);
455 log_msg(1, "bytes_read_in = %ld", bytes_read_in);
456// if (bytes_read_in!=128+64) { fatal_error("Can't read the terminating block"); }
457 fwrite(tmp, 1, bytes_read_in, ftmp);
458
459 mr_free(tmp);
460 if (!(tmp = malloc(512))) {
461 fatal_error("Failed to malloc() tmp");
462 }
463 /* BERLIOS : strange ???
464 s-printf(tmp, "I am here - %llu", ftello(fin));
465 log_msg(0, tmp);
466 */
467 fread(tmp, 1, 512, fin);
468 log_msg(0, "tmp = '%s'", tmp);
469 fwrite(tmp, 1, 512, ftmp);
470 fclose(ftmp);
471 fatal_error("Missing terminating block");
472 }
473 }
474 mr_free(tmp);
475 }
476
477 mr_free(buf);
478 log_msg(3, "Successfully copied %ld bytes", bytes_written_out);
479 return (retval);
480}
481
482/**
483 * Feed @p input_device through ntfsclone to @p output_fname.
484 * @param input_device The device to image.
485 * @param output_fname The file to write.
486 * @return 0 for success, nonzero for failure.
487 */
488int feed_into_ntfsprog(char *input_device, char *output_fname)
489{
490// BACKUP
491 int res = -1;
492 char *command = NULL;
493 char *tmp = NULL;
494
495 if (!does_file_exist(input_device)) {
496 fatal_error("input device does not exist");
497 }
498 tmp = find_home_of_exe("ntfsclone");
499 if (!tmp) {
500 fatal_error("ntfsclone not found");
501 }
502 mr_free(tmp);
503
504 mr_asprintf(&command, "ntfsclone --force --save-image --overwrite %s %s", output_fname, input_device);
505 res = run_program_and_log_output(command, 5);
506 mr_free(command);
507
508 unlink(output_fname);
509 return (res);
510}
511
512
513int run_external_binary_with_percentage_indicator_OLD(char *tt, char *cmd)
514{
515
516 int res = 0;
517 int percentage = 0;
518 int maxpc = 0;
519 int pcno = 0;
520 int last_pcno = 0;
521
522 char *command = NULL;
523 char *tempfile = NULL;
524 FILE *pin = NULL;
525
526 assert_string_is_neither_NULL_nor_zerolength(cmd);
527
528 tempfile = call_program_and_get_last_line_of_output("mktemp -q /tmp/mondo.XXXXXXXX");
529 mr_asprintf(&command, "%s >> %s 2>> %s; rm -f %s", cmd, tempfile, tempfile,
530 tempfile);
531 log_msg(3, command);
532 open_evalcall_form(tt);
533
534 if (!(pin = popen(command, "r"))) {
535 log_OS_error("fmt err");
536 mr_free(command);
537 mr_free(tempfile);
538 return (1);
539 }
540 mr_free(command);
541
542 maxpc = 100;
543// OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD
544 for (sleep(1); does_file_exist(tempfile); sleep(1)) {
545 pcno = grab_percentage_from_last_line_of_file(MONDO_LOGFILE);
546 if (pcno < 0 || pcno > 100) {
547 log_msg(5, "Weird pc#");
548 continue;
549 }
550 percentage = pcno * 100 / maxpc;
551 if (pcno <= 5 && last_pcno > 40) {
552 close_evalcall_form();
553 open_evalcall_form("_(Verifying...");
554 }
555 last_pcno = pcno;
556 update_evalcall_form(percentage);
557 }
558// OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD
559 close_evalcall_form();
560 if (pclose(pin)) {
561 res++;
562 log_OS_error("Unable to pclose");
563 }
564 unlink(tempfile);
565 mr_free(tempfile);
566 return (res);
567}
568
569
570void *run_prog_in_bkgd_then_exit(void *info)
571{
572 char *sz_command;
573 static int res = 4444;
574
575 res = 999;
576 sz_command = (char *) info;
577 log_msg(4, "sz_command = '%s'", sz_command);
578 res = system(sz_command);
579 if (res > 256 && res != 4444) {
580 res = res / 256;
581 }
582 log_msg(4, "child res = %d", res);
583 sz_command[0] = '\0';
584 pthread_exit((void *) (&res));
585}
586
587
588int run_external_binary_with_percentage_indicator_NEW(char *tt, char *cmd)
589{
590
591 /*@ int *************************************************************** */
592 int res = 0;
593 int percentage = 0;
594 int maxpc = 100;
595 int pcno = 0;
596 int last_pcno = 0;
597 int counter = 0;
598
599 /*@ buffers *********************************************************** */
600 char *command;
601 /*@ pointers ********************************************************** */
602 static int chldres = 0;
603 int *pchild_result;
604 pthread_t childthread;
605
606 pchild_result = &chldres;
607 assert_string_is_neither_NULL_nor_zerolength(cmd);
608 assert_string_is_neither_NULL_nor_zerolength(tt);
609 *pchild_result = 999;
610
611 mr_asprintf(&command, "%s 2>> %s", cmd, MONDO_LOGFILE);
612 log_msg(3, "command = '%s'", command);
613 if ((res =
614 pthread_create(&childthread, NULL, run_prog_in_bkgd_then_exit,
615 (void *) command))) {
616 fatal_error("Unable to create an archival thread");
617 }
618
619 log_msg(8, "Parent running");
620 open_evalcall_form(tt);
621 for (sleep(1); command[0] != '\0'; sleep(1)) {
622 pcno = grab_percentage_from_last_line_of_file(MONDO_LOGFILE);
623 if (pcno <= 0 || pcno > 100) {
624 log_msg(8, "Weird pc#");
625 continue;
626 }
627 percentage = pcno * 100 / maxpc;
628 if (pcno <= 5 && last_pcno >= 40) {
629 close_evalcall_form();
630 open_evalcall_form(_("Verifying..."));
631 }
632 if (counter++ >= 5) {
633 counter = 0;
634 log_file_end_to_screen(MONDO_LOGFILE, "");
635 }
636 last_pcno = pcno;
637 update_evalcall_form(percentage);
638 }
639 mr_free(command);
640
641 log_file_end_to_screen(MONDO_LOGFILE, "");
642 close_evalcall_form();
643 pthread_join(childthread, (void *) (&pchild_result));
644 if (pchild_result) {
645 res = *pchild_result;
646 } else {
647 res = 666;
648 }
649 log_msg(3, "Parent res = %d", res);
650 return (res);
651}
652
653
654/**
655 * Feed @p input_fifo through ntfsclone (restore) to @p output_device.
656 * @param input_fifo The ntfsclone file to read.
657 * @param output_device Where to put the output.
658 * @return The return value of ntfsclone (0 for success).
659 */
660int feed_outfrom_ntfsprog(char *output_device, char *input_fifo)
661{
662// RESTORE
663 int res = -1;
664 char *command = NULL;
665 char *tmp = NULL;
666
667 tmp = find_home_of_exe("ntfsclone");
668 if (!tmp) {
669 fatal_error("ntfsclone not found");
670 }
671 mr_free(tmp);
672
673 mr_asprintf(&command, "ntfsclone --force --restore-image --overwrite %s %s", output_device, input_fifo);
674 res = run_program_and_log_output(command, 5);
675 mr_free(command);
676 return (res);
677}
Note: See TracBrowser for help on using the repository browser.