2 Bacula® - The Network Backup Solution
4 Copyright (C) 2000-2009 Free Software Foundation Europe e.V.
6 The main author of Bacula is Kern Sibbald, with contributions from
7 many others, a complete list can be found in the file AUTHORS.
8 This program is Free Software; you can redistribute it and/or
9 modify it under the terms of version two of the GNU General Public
10 License as published by the Free Software Foundation and included
13 This program 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 General Public License for more details.
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
23 Bacula® is a registered trademark of Kern Sibbald.
24 The licensor of Bacula is the Free Software Foundation Europe
25 (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26 Switzerland, email:ftf@fsfeurope.org.
30 * Bacula Director -- User Agent Output Commands
31 * I.e. messages, listing database, showing resources, ...
33 * Kern Sibbald, September MM
41 /* Imported subroutines */
43 /* Imported variables */
45 /* Imported functions */
47 /* Forward referenced functions */
48 static int do_list_cmd(UAContext *ua, const char *cmd, e_list_type llist);
49 static bool list_nextvol(UAContext *ua, int ndays);
52 * Turn auto display of console messages on/off
54 int autodisplay_cmd(UAContext *ua, const char *cmd)
56 static const char *kw[] = {
61 switch (find_arg_keyword(ua, kw)) {
63 ua->auto_display_messages = true;
66 ua->auto_display_messages = false;
69 ua->error_msg(_("ON or OFF keyword missing.\n"));
76 * Turn GUI mode on/off
78 int gui_cmd(UAContext *ua, const char *cmd)
80 static const char *kw[] = {
85 switch (find_arg_keyword(ua, kw)) {
87 ua->jcr->gui = ua->gui = true;
90 ua->jcr->gui = ua->gui = false;
93 ua->error_msg(_("ON or OFF keyword missing.\n"));
100 * Enter with Resources locked
102 static void show_disabled_jobs(UAContext *ua)
106 foreach_res(job, R_JOB) {
107 if (!acl_access_ok(ua, Job_ACL, job->name())) {
113 ua->send_msg(_("Disabled Jobs:\n"));
115 ua->send_msg(" %s\n", job->name());
119 ua->send_msg(_("No disabled Jobs.\n"));
123 struct showstruct {const char *res_name; int type;};
124 static struct showstruct reses[] = {
125 {NT_("directors"), R_DIRECTOR},
126 {NT_("clients"), R_CLIENT},
127 {NT_("counters"), R_COUNTER},
128 {NT_("devices"), R_DEVICE},
129 {NT_("jobs"), R_JOB},
130 {NT_("storages"), R_STORAGE},
131 {NT_("catalogs"), R_CATALOG},
132 {NT_("schedules"), R_SCHEDULE},
133 {NT_("filesets"), R_FILESET},
134 {NT_("pools"), R_POOL},
135 {NT_("messages"), R_MSGS},
146 * show <resource-keyword-name> e.g. show directors
147 * show <resource-keyword-name>=<name> e.g. show director=HeadMan
148 * show disabled shows disabled jobs
151 int show_cmd(UAContext *ua, const char *cmd)
158 Dmsg1(20, "show: %s\n", ua->UA_sock->msg);
162 for (i=1; i<ua->argc; i++) {
163 if (strcasecmp(ua->argk[i], _("disabled")) == 0) {
164 show_disabled_jobs(ua);
168 res_name = ua->argk[i];
169 if (!ua->argv[i]) { /* was a name given? */
170 /* No name, dump all resources of specified type */
172 len = strlen(res_name);
173 for (j=0; reses[j].res_name; j++) {
174 if (strncasecmp(res_name, _(reses[j].res_name), len) == 0) {
175 type = reses[j].type;
177 res = res_head[type-r_first];
186 /* Dump a single resource with specified name */
188 len = strlen(res_name);
189 for (j=0; reses[j].res_name; j++) {
190 if (strncasecmp(res_name, _(reses[j].res_name), len) == 0) {
191 type = reses[j].type;
192 res = (RES *)GetResWithName(type, ua->argv[i]);
203 for (j=r_first; j<=r_last; j++) {
204 dump_resource(j, res_head[j-r_first], bsendmsg, ua);
208 ua->send_msg(_("Keywords for the show command are:\n"));
209 for (j=0; reses[j].res_name; j++) {
210 ua->error_msg("%s\n", _(reses[j].res_name));
214 ua->error_msg(_("%s resource %s not found.\n"), res_name, ua->argv[i]);
217 ua->error_msg(_("Resource %s not found\n"), res_name);
220 dump_resource(recurse?type:-type, res, bsendmsg, ua);
233 * List contents of database
235 * list jobs - lists all jobs run
236 * list jobid=nnn - list job data for jobid
237 * list ujobid=uname - list job data for unique jobid
238 * list job=name - list all jobs with "name"
239 * list jobname=name - same as above
240 * list jobmedia jobid=<nn>
241 * list jobmedia job=name
242 * list joblog jobid=<nn>
243 * list joblog job=name
244 * list files jobid=<nn> - list files saved for job nn
245 * list files job=name
246 * list pools - list pool records
247 * list jobtotals - list totals for all jobs
248 * list media - list media for given pool (deprecated)
249 * list volumes - list Volumes
250 * list clients - list clients
251 * list nextvol job=xx - list the next vol to be used by job
252 * list nextvolume job=xx - same as above.
253 * list copies jobid=x,y,z
257 /* Do long or full listing */
258 int llist_cmd(UAContext *ua, const char *cmd)
260 return do_list_cmd(ua, cmd, VERT_LIST);
263 /* Do short or summary listing */
264 int list_cmd(UAContext *ua, const char *cmd)
266 return do_list_cmd(ua, cmd, HORZ_LIST);
269 static int do_list_cmd(UAContext *ua, const char *cmd, e_list_type llist)
278 if (!open_client_db(ua))
281 memset(&jr, 0, sizeof(jr));
282 memset(&pr, 0, sizeof(pr));
283 memset(&mr, 0, sizeof(mr));
285 Dmsg1(20, "list: %s\n", cmd);
288 ua->error_msg(_("Hey! DB is NULL\n"));
291 /* Scan arguments looking for things to do */
292 for (i=1; i<ua->argc; i++) {
294 if (strcasecmp(ua->argk[i], NT_("jobs")) == 0) {
295 /* Apply any limit */
296 j = find_arg_with_value(ua, NT_("limit"));
298 jr.limit = atoi(ua->argv[j]);
300 db_list_job_records(ua->jcr, ua->db, &jr, prtit, ua, llist);
303 } else if (strcasecmp(ua->argk[i], NT_("jobtotals")) == 0) {
304 db_list_job_totals(ua->jcr, ua->db, &jr, prtit, ua);
307 } else if (strcasecmp(ua->argk[i], NT_("jobid")) == 0) {
309 jobid = str_to_int64(ua->argv[i]);
312 db_list_job_records(ua->jcr, ua->db, &jr, prtit, ua, llist);
317 } else if ((strcasecmp(ua->argk[i], NT_("job")) == 0 ||
318 strcasecmp(ua->argk[i], NT_("jobname")) == 0) && ua->argv[i]) {
319 bstrncpy(jr.Name, ua->argv[i], MAX_NAME_LENGTH);
321 db_list_job_records(ua->jcr, ua->db, &jr, prtit, ua, llist);
323 /* List UJOBID=xxx */
324 } else if (strcasecmp(ua->argk[i], NT_("ujobid")) == 0 && ua->argv[i]) {
325 bstrncpy(jr.Job, ua->argv[i], MAX_NAME_LENGTH);
327 db_list_job_records(ua->jcr, ua->db, &jr, prtit, ua, llist);
330 } else if (strcasecmp(ua->argk[i], NT_("files")) == 0) {
332 for (j=i+1; j<ua->argc; j++) {
333 if (strcasecmp(ua->argk[j], NT_("ujobid")) == 0 && ua->argv[j]) {
334 bstrncpy(jr.Job, ua->argv[j], MAX_NAME_LENGTH);
336 db_get_job_record(ua->jcr, ua->db, &jr);
338 } else if (strcasecmp(ua->argk[j], NT_("jobid")) == 0 && ua->argv[j]) {
339 jobid = str_to_int64(ua->argv[j]);
344 db_list_files_for_job(ua->jcr, ua->db, jobid, prtit, ua);
349 } else if (strcasecmp(ua->argk[i], NT_("jobmedia")) == 0) {
351 for (j=i+1; j<ua->argc; j++) {
352 if (strcasecmp(ua->argk[j], NT_("ujobid")) == 0 && ua->argv[j]) {
353 bstrncpy(jr.Job, ua->argv[j], MAX_NAME_LENGTH);
355 db_get_job_record(ua->jcr, ua->db, &jr);
357 } else if (strcasecmp(ua->argk[j], NT_("jobid")) == 0 && ua->argv[j]) {
358 jobid = str_to_int64(ua->argv[j]);
362 db_list_jobmedia_records(ua->jcr, ua->db, jobid, prtit, ua, llist);
366 /* List for all jobs (jobid=0) */
367 db_list_jobmedia_records(ua->jcr, ua->db, 0, prtit, ua, llist);
371 } else if (strcasecmp(ua->argk[i], NT_("joblog")) == 0) {
373 for (j=i+1; j<ua->argc; j++) {
374 if (strcasecmp(ua->argk[j], NT_("ujobid")) == 0 && ua->argv[j]) {
375 bstrncpy(jr.Job, ua->argv[j], MAX_NAME_LENGTH);
377 db_get_job_record(ua->jcr, ua->db, &jr);
379 } else if (strcasecmp(ua->argk[j], NT_("jobid")) == 0 && ua->argv[j]) {
380 jobid = str_to_int64(ua->argv[j]);
384 db_list_joblog_records(ua->jcr, ua->db, jobid, prtit, ua, llist);
388 /* List for all jobs (jobid=0) */
389 db_list_joblog_records(ua->jcr, ua->db, 0, prtit, ua, llist);
394 } else if (strcasecmp(ua->argk[i], NT_("pool")) == 0 ||
395 strcasecmp(ua->argk[i], NT_("pools")) == 0) {
397 memset(&pr, 0, sizeof(pr));
399 bstrncpy(pr.Name, ua->argv[i], sizeof(pr.Name));
401 db_list_pool_records(ua->jcr, ua->db, &pr, prtit, ua, llist);
403 } else if (strcasecmp(ua->argk[i], NT_("clients")) == 0) {
404 db_list_client_records(ua->jcr, ua->db, prtit, ua, llist);
407 /* List MEDIA or VOLUMES */
408 } else if (strcasecmp(ua->argk[i], NT_("media")) == 0 ||
409 strcasecmp(ua->argk[i], NT_("volume")) == 0 ||
410 strcasecmp(ua->argk[i], NT_("volumes")) == 0) {
412 for (j=i+1; j<ua->argc; j++) {
413 if (strcasecmp(ua->argk[j], NT_("ujobid")) == 0 && ua->argv[j]) {
414 bstrncpy(jr.Job, ua->argv[j], MAX_NAME_LENGTH);
416 db_get_job_record(ua->jcr, ua->db, &jr);
418 } else if (strcasecmp(ua->argk[j], NT_("jobid")) == 0 && ua->argv[j]) {
419 jobid = str_to_int64(ua->argv[j]);
423 VolumeName = get_pool_memory(PM_FNAME);
424 n = db_get_job_volume_names(ua->jcr, ua->db, jobid, &VolumeName);
425 ua->send_msg(_("Jobid %d used %d Volume(s): %s\n"), jobid, n, VolumeName);
426 free_pool_memory(VolumeName);
429 /* if no job or jobid keyword found, then we list all media */
433 /* List a specific volume? */
435 bstrncpy(mr.VolumeName, ua->argv[i], sizeof(mr.VolumeName));
436 db_list_media_records(ua->jcr, ua->db, &mr, prtit, ua, llist);
439 /* Is a specific pool wanted? */
440 for (i=1; i<ua->argc; i++) {
441 if (strcasecmp(ua->argk[i], NT_("pool")) == 0) {
442 if (!get_pool_dbr(ua, &pr)) {
443 ua->error_msg(_("No Pool specified.\n"));
446 mr.PoolId = pr.PoolId;
447 db_list_media_records(ua->jcr, ua->db, &mr, prtit, ua, llist);
452 /* List Volumes in all pools */
453 if (!db_get_pool_ids(ua->jcr, ua->db, &num_pools, &ids)) {
454 ua->error_msg(_("Error obtaining pool ids. ERR=%s\n"),
455 db_strerror(ua->db));
458 if (num_pools <= 0) {
461 for (i=0; i < num_pools; i++) {
463 if (db_get_pool_record(ua->jcr, ua->db, &pr)) {
464 ua->send_msg(_("Pool: %s\n"), pr.Name);
467 db_list_media_records(ua->jcr, ua->db, &mr, prtit, ua, llist);
472 /* List next volume */
473 } else if (strcasecmp(ua->argk[i], NT_("nextvol")) == 0 ||
474 strcasecmp(ua->argk[i], NT_("nextvolume")) == 0) {
476 j = find_arg_with_value(ua, NT_("days"));
478 n = atoi(ua->argv[j]);
479 if ((n < 0) || (n > 50)) {
480 ua->warning_msg(_("Ignoring invalid value for days. Max is 50.\n"));
485 } else if (strcasecmp(ua->argk[i], NT_("copies")) == 0) {
488 for (j=i+1; j<ua->argc; j++) {
489 if (strcasecmp(ua->argk[j], NT_("jobid")) == 0 && ua->argv[j]) {
490 if (is_a_number_list(ua->argv[j])) {
491 jobids = ua->argv[j];
493 } else if (strcasecmp(ua->argk[j], NT_("limit")) == 0 && ua->argv[j]) {
494 limit = atoi(ua->argv[j]);
497 db_list_copies_records(ua->jcr,ua->db,limit,jobids,prtit,ua,llist);
498 } else if (strcasecmp(ua->argk[i], NT_("limit")) == 0
499 || strcasecmp(ua->argk[i], NT_("days")) == 0) {
502 ua->error_msg(_("Unknown list keyword: %s\n"), NPRT(ua->argk[i]));
508 static bool list_nextvol(UAContext *ua, int ndays)
519 memset(&mr, 0, sizeof(mr));
520 int i = find_arg_with_value(ua, "job");
522 if ((job = select_job_resource(ua)) == NULL) {
526 job = (JOB *)GetResWithName(R_JOB, ua->argv[i]);
528 Jmsg(ua->jcr, M_ERROR, 0, _("%s is not a job name.\n"), ua->argv[i]);
529 if ((job = select_job_resource(ua)) == NULL) {
535 jcr = new_jcr(sizeof(JCR), dird_free_jcr);
536 for (run=NULL; (run = find_next_run(run, job, runtime, ndays)); ) {
537 if (!complete_jcr_for_job(jcr, job, run->pool)) {
541 if (!jcr->jr.PoolId) {
542 ua->error_msg(_("Could not find Pool for Job %s\n"), job->name());
545 memset(&pr, 0, sizeof(pr));
546 pr.PoolId = jcr->jr.PoolId;
547 if (!db_get_pool_record(jcr, jcr->db, &pr)) {
548 bstrncpy(pr.Name, "*UnknownPool*", sizeof(pr.Name));
550 mr.PoolId = jcr->jr.PoolId;
551 get_job_storage(&store, job, run);
552 mr.StorageId = store.store->StorageId;
553 /* no need to set ScratchPoolId, since we use fnv_no_create_vol */
554 if (!find_next_volume_for_append(jcr, &mr, 1, fnv_no_create_vol, fnv_prune)) {
555 ua->error_msg(_("Could not find next Volume for Job %s (Pool=%s, Level=%s).\n"),
556 job->name(), pr.Name, level_to_str(run->level));
559 _("The next Volume to be used by Job \"%s\" (Pool=%s, Level=%s) will be %s\n"),
560 job->name(), pr.Name, level_to_str(run->level), mr.VolumeName);
566 db_close_database(jcr, jcr->db);
570 ua->error_msg(_("Could not find next Volume for Job %s.\n"),
579 * For a given job, we examine all his run records
580 * to see if it is scheduled today or tomorrow.
582 RUN *find_next_run(RUN *run, JOB *job, utime_t &runtime, int ndays)
584 time_t now, future, endtime;
587 int mday, wday, month, wom, i;
592 sched = job->schedule;
593 if (sched == NULL) { /* scheduled? */
594 return NULL; /* no nothing to report */
597 /* Break down the time into components */
599 endtime = now + (ndays * 60 * 60 * 24);
606 for ( ; run; run=run->next) {
608 * Find runs in next 24 hours. Day 0 is today, so if
609 * ndays=1, look at today and tomorrow.
611 for (day = 0; day <= ndays; day++) {
612 future = now + (day * 60 * 60 * 24);
614 /* Break down the time into components */
615 (void)localtime_r(&future, &tm);
616 mday = tm.tm_mday - 1;
620 woy = tm_woy(future);
622 is_scheduled = bit_is_set(mday, run->mday) && bit_is_set(wday, run->wday) &&
623 bit_is_set(month, run->month) && bit_is_set(wom, run->wom) &&
624 bit_is_set(woy, run->woy);
627 Pmsg2(000, "day=%d is_scheduled=%d\n", day, is_scheduled);
628 Pmsg1(000, "bit_set_mday=%d\n", bit_is_set(mday, run->mday));
629 Pmsg1(000, "bit_set_wday=%d\n", bit_is_set(wday, run->wday));
630 Pmsg1(000, "bit_set_month=%d\n", bit_is_set(month, run->month));
631 Pmsg1(000, "bit_set_wom=%d\n", bit_is_set(wom, run->wom));
632 Pmsg1(000, "bit_set_woy=%d\n", bit_is_set(woy, run->woy));
635 if (is_scheduled) { /* Jobs scheduled on that day */
637 char buf[300], num[10];
638 bsnprintf(buf, sizeof(buf), "tm.hour=%d hour=", tm.tm_hour);
639 for (i=0; i<24; i++) {
640 if (bit_is_set(i, run->hour)) {
641 bsnprintf(num, sizeof(num), "%d ", i);
642 bstrncat(buf, num, sizeof(buf));
645 bstrncat(buf, "\n", sizeof(buf));
646 Pmsg1(000, "%s", buf);
648 /* find time (time_t) job is to be run */
649 (void)localtime_r(&future, &runtm);
650 for (i= 0; i < 24; i++) {
651 if (bit_is_set(i, run->hour)) {
653 runtm.tm_min = run->minute;
655 runtime = mktime(&runtm);
656 Dmsg2(200, "now=%d runtime=%lld\n", now, runtime);
657 if ((runtime > now) && (runtime < endtime)) {
658 Dmsg2(200, "Found it level=%d %c\n", run->level, run->level);
659 return run; /* found it, return run resource */
665 } /* end for loop over runs */
671 * Fill in the remaining fields of the jcr as if it
672 * is going to run the job.
674 bool complete_jcr_for_job(JCR *jcr, JOB *job, POOL *pool)
678 memset(&pr, 0, sizeof(POOL_DBR));
679 set_jcr_defaults(jcr, job);
681 jcr->pool = pool; /* override */
684 Dmsg0(100, "complete_jcr close db\n");
685 db_close_database(jcr, jcr->db);
689 Dmsg0(100, "complete_jcr open db\n");
690 jcr->db = jcr->db=db_init(jcr, jcr->catalog->db_driver, jcr->catalog->db_name,
691 jcr->catalog->db_user,
692 jcr->catalog->db_password, jcr->catalog->db_address,
693 jcr->catalog->db_port, jcr->catalog->db_socket,
694 jcr->catalog->mult_db_connections);
695 if (!jcr->db || !db_open_database(jcr, jcr->db)) {
696 Jmsg(jcr, M_FATAL, 0, _("Could not open database \"%s\".\n"),
697 jcr->catalog->db_name);
699 Jmsg(jcr, M_FATAL, 0, "%s", db_strerror(jcr->db));
700 db_close_database(jcr, jcr->db);
705 bstrncpy(pr.Name, jcr->pool->name(), sizeof(pr.Name));
706 while (!db_get_pool_record(jcr, jcr->db, &pr)) { /* get by Name */
707 /* Try to create the pool */
708 if (create_pool(jcr, jcr->db, jcr->pool, POOL_OP_CREATE) < 0) {
709 Jmsg(jcr, M_FATAL, 0, _("Pool %s not in database. %s"), pr.Name,
710 db_strerror(jcr->db));
712 db_close_database(jcr, jcr->db);
717 Jmsg(jcr, M_INFO, 0, _("Pool %s created in database.\n"), pr.Name);
720 jcr->jr.PoolId = pr.PoolId;
725 static void con_lock_release(void *arg)
730 void do_messages(UAContext *ua, const char *cmd)
734 bool do_truncate = false;
737 pthread_cleanup_push(con_lock_release, (void *)NULL);
739 while (fgets(msg, sizeof(msg), con_fd)) {
741 ua->UA_sock->msg = check_pool_memory_size(ua->UA_sock->msg, mlen+1);
742 strcpy(ua->UA_sock->msg, msg);
743 ua->UA_sock->msglen = mlen;
748 (void)ftruncate(fileno(con_fd), 0L);
750 console_msg_pending = FALSE;
751 ua->user_notified_msg_pending = FALSE;
752 pthread_cleanup_pop(0);
757 int qmessagescmd(UAContext *ua, const char *cmd)
759 if (console_msg_pending && ua->auto_display_messages) {
760 do_messages(ua, cmd);
765 int messagescmd(UAContext *ua, const char *cmd)
767 if (console_msg_pending) {
768 do_messages(ua, cmd);
770 ua->UA_sock->fsend(_("You have no messages.\n"));
776 * Callback routine for "printing" database file listing
778 void prtit(void *ctx, const char *msg)
780 UAContext *ua = (UAContext *)ctx;
782 ua->UA_sock->fsend("%s", msg);
786 * Format message and send to other end.
788 * If the UA_sock is NULL, it means that there is no user
789 * agent, so we are being called from Bacula core. In
790 * that case direct the messages to the Job.
793 void bmsg(UAContext *ua, const char *fmt, va_list arg_ptr)
795 BSOCK *bs = ua->UA_sock;
804 msg = get_pool_memory(PM_EMSG);
808 maxlen = sizeof_pool_memory(msg) - 1;
809 va_copy(ap, arg_ptr);
810 len = bvsnprintf(msg, maxlen, fmt, ap);
812 if (len < 0 || len >= maxlen) {
813 msg = realloc_pool_memory(msg, maxlen + maxlen/2);
821 } else { /* No UA, send to Job */
822 Jmsg(ua->jcr, M_INFO, 0, "%s", msg);
823 free_pool_memory(msg);
828 #else /* no va_copy() -- brain damaged version of variable arguments */
830 void bmsg(UAContext *ua, const char *fmt, va_list arg_ptr)
832 BSOCK *bs = ua->UA_sock;
840 msg = get_memory(5000);
843 maxlen = sizeof_pool_memory(msg) - 1;
845 msg = realloc_pool_memory(msg, 5000);
848 len = bvsnprintf(msg, maxlen, fmt, arg_ptr);
849 if (len < 0 || len >= maxlen) {
850 pm_strcpy(msg, _("Message too long to display.\n"));
858 } else { /* No UA, send to Job */
859 Jmsg(ua->jcr, M_INFO, 0, "%s", msg);
860 free_pool_memory(msg);
866 void bsendmsg(void *ctx, const char *fmt, ...)
869 va_start(arg_ptr, fmt);
870 bmsg((UAContext *)ctx, fmt, arg_ptr);
875 * The following UA methods are mainly intended for GUI
879 * This is a message that should be displayed on the user's
882 void UAContext::send_msg(const char *fmt, ...)
885 va_start(arg_ptr, fmt);
886 bmsg(this, fmt, arg_ptr);
892 * This is an error condition with a command. The gui should put
893 * up an error or critical dialog box. The command is aborted.
895 void UAContext::error_msg(const char *fmt, ...)
900 if (bs && api) bs->signal(BNET_ERROR_MSG);
901 va_start(arg_ptr, fmt);
902 bmsg(this, fmt, arg_ptr);
907 * This is a warning message, that should bring up a warning
908 * dialog box on the GUI. The command is not aborted, but something
911 void UAContext::warning_msg(const char *fmt, ...)
916 if (bs && api) bs->signal(BNET_WARNING_MSG);
917 va_start(arg_ptr, fmt);
918 bmsg(this, fmt, arg_ptr);
923 * This is an information message that should probably be put
924 * into the status line of a GUI program.
926 void UAContext::info_msg(const char *fmt, ...)
931 if (bs && api) bs->signal(BNET_INFO_MSG);
932 va_start(arg_ptr, fmt);
933 bmsg(this, fmt, arg_ptr);