]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_status.c
kes Extend new GUI api code to tree commands.
[bacula/bacula] / bacula / src / dird / ua_status.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2001-2007 Free Software Foundation Europe e.V.
5
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 plus additions
11    that are listed in the file LICENSE.
12
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.
17
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
21    02110-1301, USA.
22
23    Bacula® is a registered trademark of John Walker.
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.
27 */
28 /*
29  *
30  *   Bacula Director -- User Agent Status Command
31  *
32  *     Kern Sibbald, August MMI
33  *
34  *   Version $Id$
35  */
36
37
38 #include "bacula.h"
39 #include "dird.h"
40
41 static void list_scheduled_jobs(UAContext *ua);
42 static void list_running_jobs(UAContext *ua);
43 static void list_terminated_jobs(UAContext *ua);
44 static void do_storage_status(UAContext *ua, STORE *store);
45 static void do_client_status(UAContext *ua, CLIENT *client);
46 static void do_director_status(UAContext *ua);
47 static void do_all_status(UAContext *ua);
48
49 static char OKqstatus[]   = "1000 OK .status\n";
50 static char DotStatusJob[] = "JobId=%s JobStatus=%c JobErrors=%d\n";
51
52 /*
53  * .status command
54  */
55
56 bool dot_status_cmd(UAContext *ua, const char *cmd)
57 {
58    JCR* njcr = NULL;
59    s_last_job* job;
60    char ed1[50];
61
62    Dmsg1(20, "status:%s:\n", cmd);
63
64    if ((ua->argc != 3) || (strcasecmp(ua->argk[1], "dir"))) {
65       bsendmsg(ua, "1900 Bad .status command, missing arguments.\n");
66       return false;
67    }
68
69    if (strcasecmp(ua->argk[2], "current") == 0) {
70       bsendmsg(ua, OKqstatus, ua->argk[2]);
71       foreach_jcr(njcr) {
72          if (njcr->JobId != 0 && acl_access_ok(ua, Job_ACL, njcr->job->name())) {
73             bsendmsg(ua, DotStatusJob, edit_int64(njcr->JobId, ed1), 
74                      njcr->JobStatus, njcr->JobErrors);
75          }
76       }
77       endeach_jcr(njcr);
78    } else if (strcasecmp(ua->argk[2], "last") == 0) {
79       bsendmsg(ua, OKqstatus, ua->argk[2]);
80       if ((last_jobs) && (last_jobs->size() > 0)) {
81          job = (s_last_job*)last_jobs->last();
82          if (acl_access_ok(ua, Job_ACL, job->Job)) {
83             bsendmsg(ua, DotStatusJob, edit_int64(job->JobId, ed1), 
84                   job->JobStatus, job->Errors);
85          }
86       }
87    } else {
88       bsendmsg(ua, "1900 Bad .status command, wrong argument.\n");
89       return false;
90    }
91
92    return true;
93 }
94
95 /* This is the *old* command handler, so we must return
96  *  1 or it closes the connection
97  */
98 int qstatus_cmd(UAContext *ua, const char *cmd)
99 {
100    dot_status_cmd(ua, cmd);
101    return 1;
102 }
103
104 /*
105  * status command
106  */
107 int status_cmd(UAContext *ua, const char *cmd)
108 {
109    STORE *store;
110    CLIENT *client;
111    int item, i;
112
113    Dmsg1(20, "status:%s:\n", cmd);
114
115    for (i=1; i<ua->argc; i++) {
116       if (strcasecmp(ua->argk[i], NT_("all")) == 0) {
117          do_all_status(ua);
118          return 1;
119       } else if (strcasecmp(ua->argk[i], NT_("dir")) == 0 ||
120                  strcasecmp(ua->argk[i], NT_("director")) == 0) {
121          do_director_status(ua);
122          return 1;
123       } else if (strcasecmp(ua->argk[i], NT_("client")) == 0) {
124          client = get_client_resource(ua);
125          if (client) {
126             do_client_status(ua, client);
127          }
128          return 1;
129       } else {
130          store = get_storage_resource(ua, false/*no default*/);
131          if (store) {
132             do_storage_status(ua, store);
133          }
134          return 1;
135       }
136    }
137    /* If no args, ask for status type */
138    if (ua->argc == 1) {
139        char prmt[MAX_NAME_LENGTH];
140
141       start_prompt(ua, _("Status available for:\n"));
142       add_prompt(ua, NT_("Director"));
143       add_prompt(ua, NT_("Storage"));
144       add_prompt(ua, NT_("Client"));
145       add_prompt(ua, NT_("All"));
146       Dmsg0(20, "do_prompt: select daemon\n");
147       if ((item=do_prompt(ua, "",  _("Select daemon type for status"), prmt, sizeof(prmt))) < 0) {
148          return 1;
149       }
150       Dmsg1(20, "item=%d\n", item);
151       switch (item) {
152       case 0:                         /* Director */
153          do_director_status(ua);
154          break;
155       case 1:
156          store = select_storage_resource(ua);
157          if (store) {
158             do_storage_status(ua, store);
159          }
160          break;
161       case 2:
162          client = select_client_resource(ua);
163          if (client) {
164             do_client_status(ua, client);
165          }
166          break;
167       case 3:
168          do_all_status(ua);
169          break;
170       default:
171          break;
172       }
173    }
174    return 1;
175 }
176
177 static void do_all_status(UAContext *ua)
178 {
179    STORE *store, **unique_store;
180    CLIENT *client, **unique_client;
181    int i, j;
182    bool found;
183
184    do_director_status(ua);
185
186    /* Count Storage items */
187    LockRes();
188    i = 0;
189    foreach_res(store, R_STORAGE) {
190       i++;
191    }
192    unique_store = (STORE **) malloc(i * sizeof(STORE));
193    /* Find Unique Storage address/port */
194    i = 0;
195    foreach_res(store, R_STORAGE) {
196       found = false;
197       if (!acl_access_ok(ua, Storage_ACL, store->name())) {
198          continue;
199       }
200       for (j=0; j<i; j++) {
201          if (strcmp(unique_store[j]->address, store->address) == 0 &&
202              unique_store[j]->SDport == store->SDport) {
203             found = true;
204             break;
205          }
206       }
207       if (!found) {
208          unique_store[i++] = store;
209          Dmsg2(40, "Stuffing: %s:%d\n", store->address, store->SDport);
210       }
211    }
212    UnlockRes();
213
214    /* Call each unique Storage daemon */
215    for (j=0; j<i; j++) {
216       do_storage_status(ua, unique_store[j]);
217    }
218    free(unique_store);
219
220    /* Count Client items */
221    LockRes();
222    i = 0;
223    foreach_res(client, R_CLIENT) {
224       i++;
225    }
226    unique_client = (CLIENT **)malloc(i * sizeof(CLIENT));
227    /* Find Unique Client address/port */
228    i = 0;
229    foreach_res(client, R_CLIENT) {
230       found = false;
231       if (!acl_access_ok(ua, Client_ACL, client->name())) {
232          continue;
233       }
234       for (j=0; j<i; j++) {
235          if (strcmp(unique_client[j]->address, client->address) == 0 &&
236              unique_client[j]->FDport == client->FDport) {
237             found = true;
238             break;
239          }
240       }
241       if (!found) {
242          unique_client[i++] = client;
243          Dmsg2(40, "Stuffing: %s:%d\n", client->address, client->FDport);
244       }
245    }
246    UnlockRes();
247
248    /* Call each unique File daemon */
249    for (j=0; j<i; j++) {
250       do_client_status(ua, unique_client[j]);
251    }
252    free(unique_client);
253
254 }
255
256 static void do_director_status(UAContext *ua)
257 {
258    char dt[MAX_TIME_LENGTH];
259    char b1[35], b2[35], b3[35], b4[35];
260
261    bsendmsg(ua, _("%s Version: %s (%s) %s %s %s\n"), my_name, VERSION, BDATE,
262             HOST_OS, DISTNAME, DISTVER);
263    bstrftime_nc(dt, sizeof(dt), daemon_start_time);
264    if (num_jobs_run == 1) {
265       bsendmsg(ua, _("Daemon started %s, 1 Job run since started.\n"), dt);
266    }
267    else {
268       bsendmsg(ua, _("Daemon started %s, %d Jobs run since started.\n"),
269         dt, num_jobs_run);
270    }
271    bsendmsg(ua, _(" Heap: bytes=%s max_bytes=%s bufs=%s max_bufs=%s\n"),
272             edit_uint64_with_commas(sm_bytes, b1),
273             edit_uint64_with_commas(sm_max_bytes, b2),
274             edit_uint64_with_commas(sm_buffers, b3),
275             edit_uint64_with_commas(sm_max_buffers, b4));
276
277    /*
278     * List scheduled Jobs
279     */
280    list_scheduled_jobs(ua);
281
282    /*
283     * List running jobs
284     */
285    list_running_jobs(ua);
286
287    /*
288     * List terminated jobs
289     */
290    list_terminated_jobs(ua);
291    bsendmsg(ua, _("====\n"));
292 }
293
294 static void do_storage_status(UAContext *ua, STORE *store)
295 {
296    BSOCK *sd;
297    USTORE lstore;
298
299    lstore.store = store;
300    pm_strcpy(lstore.store_source, _("unknown source"));
301    set_wstorage(ua->jcr, &lstore);
302    /* Try connecting for up to 15 seconds */
303    bsendmsg(ua, _("Connecting to Storage daemon %s at %s:%d\n"),
304       store->name(), store->address, store->SDport);
305    if (!connect_to_storage_daemon(ua->jcr, 1, 15, 0)) {
306       bsendmsg(ua, _("\nFailed to connect to Storage daemon %s.\n====\n"),
307          store->name());
308       if (ua->jcr->store_bsock) {
309          bnet_close(ua->jcr->store_bsock);
310          ua->jcr->store_bsock = NULL;
311       }
312       return;
313    }
314    Dmsg0(20, _("Connected to storage daemon\n"));
315    sd = ua->jcr->store_bsock;
316    bnet_fsend(sd, "status");
317    while (bnet_recv(sd) >= 0) {
318       bsendmsg(ua, "%s", sd->msg);
319    }
320    bnet_sig(sd, BNET_TERMINATE);
321    bnet_close(sd);
322    ua->jcr->store_bsock = NULL;
323    return;
324 }
325
326 static void do_client_status(UAContext *ua, CLIENT *client)
327 {
328    BSOCK *fd;
329
330    /* Connect to File daemon */
331
332    ua->jcr->client = client;
333    /* Release any old dummy key */
334    if (ua->jcr->sd_auth_key) {
335       free(ua->jcr->sd_auth_key);
336    }
337    /* Create a new dummy SD auth key */
338    ua->jcr->sd_auth_key = bstrdup("dummy");
339
340    /* Try to connect for 15 seconds */
341    bsendmsg(ua, _("Connecting to Client %s at %s:%d\n"),
342       client->name(), client->address, client->FDport);
343    if (!connect_to_file_daemon(ua->jcr, 1, 15, 0)) {
344       bsendmsg(ua, _("Failed to connect to Client %s.\n====\n"),
345          client->name());
346       if (ua->jcr->file_bsock) {
347          bnet_close(ua->jcr->file_bsock);
348          ua->jcr->file_bsock = NULL;
349       }
350       return;
351    }
352    Dmsg0(20, _("Connected to file daemon\n"));
353    fd = ua->jcr->file_bsock;
354    bnet_fsend(fd, "status");
355    while (bnet_recv(fd) >= 0) {
356       bsendmsg(ua, "%s", fd->msg);
357    }
358    bnet_sig(fd, BNET_TERMINATE);
359    bnet_close(fd);
360    ua->jcr->file_bsock = NULL;
361
362    return;
363 }
364
365 static void prt_runhdr(UAContext *ua)
366 {
367    bsendmsg(ua, _("\nScheduled Jobs:\n"));
368    bsendmsg(ua, _("Level          Type     Pri  Scheduled          Name               Volume\n"));
369    bsendmsg(ua, _("===================================================================================\n"));
370 }
371
372 /* Scheduling packet */
373 struct sched_pkt {
374    dlink link;                        /* keep this as first item!!! */
375    JOB *job;
376    int level;
377    int priority;
378    time_t runtime;
379    POOL *pool;
380    STORE *store;
381 };
382
383 static void prt_runtime(UAContext *ua, sched_pkt *sp)
384 {
385    char dt[MAX_TIME_LENGTH];
386    const char *level_ptr;
387    bool ok = false;
388    bool close_db = false;
389    JCR *jcr = ua->jcr;
390    MEDIA_DBR mr;
391
392    memset(&mr, 0, sizeof(mr));
393    if (sp->job->JobType == JT_BACKUP) {
394       jcr->db = NULL;
395       ok = complete_jcr_for_job(jcr, sp->job, sp->pool);
396       if (jcr->db) {
397          close_db = true;             /* new db opened, remember to close it */
398       }
399       if (ok) {
400          mr.PoolId = jcr->jr.PoolId;
401          mr.StorageId = sp->store->StorageId;
402          jcr->wstore = sp->store;
403          ok = find_next_volume_for_append(jcr, &mr, 1, false/*no create*/);
404       }
405       if (!ok) {
406          bstrncpy(mr.VolumeName, "*unknown*", sizeof(mr.VolumeName));
407       }
408    }
409    bstrftime_nc(dt, sizeof(dt), sp->runtime);
410    switch (sp->job->JobType) {
411    case JT_ADMIN:
412    case JT_RESTORE:
413       level_ptr = " ";
414       break;
415    default:
416       level_ptr = level_to_str(sp->level);
417       break;
418    }
419    bsendmsg(ua, _("%-14s %-8s %3d  %-18s %-18s %s\n"),
420       level_ptr, job_type_to_str(sp->job->JobType), sp->priority, dt,
421       sp->job->name(), mr.VolumeName);
422    if (close_db) {
423       db_close_database(jcr, jcr->db);
424    }
425    jcr->db = ua->db;                  /* restore ua db to jcr */
426 }
427
428 /*
429  * Sort items by runtime, priority
430  */
431 static int my_compare(void *item1, void *item2)
432 {
433    sched_pkt *p1 = (sched_pkt *)item1;
434    sched_pkt *p2 = (sched_pkt *)item2;
435    if (p1->runtime < p2->runtime) {
436       return -1;
437    } else if (p1->runtime > p2->runtime) {
438       return 1;
439    }
440    if (p1->priority < p2->priority) {
441       return -1;
442    } else if (p1->priority > p2->priority) {
443       return 1;
444    }
445    return 0;
446 }
447
448 /*
449  * Find all jobs to be run in roughly the
450  *  next 24 hours.
451  */
452 static void list_scheduled_jobs(UAContext *ua)
453 {
454    time_t runtime;
455    RUN *run;
456    JOB *job;
457    int level, num_jobs = 0;
458    int priority;
459    bool hdr_printed = false;
460    dlist sched;
461    sched_pkt *sp;
462    int days, i;
463
464    Dmsg0(200, "enter list_sched_jobs()\n");
465
466    days = 1;
467    i = find_arg_with_value(ua, NT_("days"));
468    if (i >= 0) {
469      days = atoi(ua->argv[i]);
470      if ((days < 0) || (days > 50)) {
471        bsendmsg(ua, _("Ignoring invalid value for days. Max is 50.\n"));
472        days = 1;
473      }
474    }
475
476    /* Loop through all jobs */
477    LockRes();
478    foreach_res(job, R_JOB) {
479       if (!acl_access_ok(ua, Job_ACL, job->name()) || !job->enabled) {
480          continue;
481       }
482       for (run=NULL; (run = find_next_run(run, job, runtime, days)); ) {
483          USTORE store;
484          level = job->JobLevel;
485          if (run->level) {
486             level = run->level;
487          }
488          priority = job->Priority;
489          if (run->Priority) {
490             priority = run->Priority;
491          }
492          if (!hdr_printed) {
493             prt_runhdr(ua);
494             hdr_printed = true;
495          }
496          sp = (sched_pkt *)malloc(sizeof(sched_pkt));
497          sp->job = job;
498          sp->level = level;
499          sp->priority = priority;
500          sp->runtime = runtime;
501          sp->pool = run->pool;
502          get_job_storage(&store, job, run);
503          sp->store = store.store;
504          sched.binary_insert_multiple(sp, my_compare);
505          num_jobs++;
506       }
507    } /* end for loop over resources */
508    UnlockRes();
509    foreach_dlist(sp, &sched) {
510       prt_runtime(ua, sp);
511    }
512    if (num_jobs == 0) {
513       bsendmsg(ua, _("No Scheduled Jobs.\n"));
514    }
515    bsendmsg(ua, _("====\n"));
516    Dmsg0(200, "Leave list_sched_jobs_runs()\n");
517 }
518
519 static void list_running_jobs(UAContext *ua)
520 {
521    JCR *jcr;
522    int njobs = 0;
523    const char *msg;
524    char *emsg;                        /* edited message */
525    char dt[MAX_TIME_LENGTH];
526    char level[10];
527    bool pool_mem = false;
528
529    Dmsg0(200, "enter list_run_jobs()\n");
530    bsendmsg(ua, _("\nRunning Jobs:\n"));
531    foreach_jcr(jcr) {
532       if (jcr->JobId == 0) {      /* this is us */
533          /* this is a console or other control job. We only show console
534           * jobs in the status output.
535           */
536          if (jcr->JobType == JT_CONSOLE) {
537             bstrftime_nc(dt, sizeof(dt), jcr->start_time);
538             bsendmsg(ua, _("Console connected at %s\n"), dt);
539          }
540          continue;
541       }       
542       njobs++;
543    }
544    endeach_jcr(jcr);
545
546    if (njobs == 0) {
547       /* Note the following message is used in regress -- don't change */
548       bsendmsg(ua, _("No Jobs running.\n====\n"));
549       Dmsg0(200, "leave list_run_jobs()\n");
550       return;
551    }
552    njobs = 0;
553    bsendmsg(ua, _(" JobId Level   Name                       Status\n"));
554    bsendmsg(ua, _("======================================================================\n"));
555    foreach_jcr(jcr) {
556       if (jcr->JobId == 0 || !acl_access_ok(ua, Job_ACL, jcr->job->name())) {
557          continue;
558       }
559       njobs++;
560       switch (jcr->JobStatus) {
561       case JS_Created:
562          msg = _("is waiting execution");
563          break;
564       case JS_Running:
565          msg = _("is running");
566          break;
567       case JS_Blocked:
568          msg = _("is blocked");
569          break;
570       case JS_Terminated:
571          msg = _("has terminated");
572          break;
573       case JS_ErrorTerminated:
574          msg = _("has erred");
575          break;
576       case JS_Error:
577          msg = _("has errors");
578          break;
579       case JS_FatalError:
580          msg = _("has a fatal error");
581          break;
582       case JS_Differences:
583          msg = _("has verify differences");
584          break;
585       case JS_Canceled:
586          msg = _("has been canceled");
587          break;
588       case JS_WaitFD:
589          emsg = (char *) get_pool_memory(PM_FNAME);
590          Mmsg(emsg, _("is waiting on Client %s"), jcr->client->name());
591          pool_mem = true;
592          msg = emsg;
593          break;
594       case JS_WaitSD:
595          emsg = (char *) get_pool_memory(PM_FNAME);
596          if (jcr->wstore) {
597             Mmsg(emsg, _("is waiting on Storage %s"), jcr->wstore->name());
598          } else {
599             Mmsg(emsg, _("is waiting on Storage %s"), jcr->rstore->name());
600          }
601          pool_mem = true;
602          msg = emsg;
603          break;
604       case JS_WaitStoreRes:
605          msg = _("is waiting on max Storage jobs");
606          break;
607       case JS_WaitClientRes:
608          msg = _("is waiting on max Client jobs");
609          break;
610       case JS_WaitJobRes:
611          msg = _("is waiting on max Job jobs");
612          break;
613       case JS_WaitMaxJobs:
614          msg = _("is waiting on max total jobs");
615          break;
616       case JS_WaitStartTime:
617          msg = _("is waiting for its start time");
618          break;
619       case JS_WaitPriority:
620          msg = _("is waiting for higher priority jobs to finish");
621          break;
622
623       default:
624          emsg = (char *) get_pool_memory(PM_FNAME);
625          Mmsg(emsg, _("is in unknown state %c"), jcr->JobStatus);
626          pool_mem = true;
627          msg = emsg;
628          break;
629       }
630       /*
631        * Now report Storage daemon status code
632        */
633       switch (jcr->SDJobStatus) {
634       case JS_WaitMount:
635          if (pool_mem) {
636             free_pool_memory(emsg);
637             pool_mem = false;
638          }
639          msg = _("is waiting for a mount request");
640          break;
641       case JS_WaitMedia:
642          if (pool_mem) {
643             free_pool_memory(emsg);
644             pool_mem = false;
645          }
646          msg = _("is waiting for an appendable Volume");
647          break;
648       case JS_WaitFD:
649          if (!pool_mem) {
650             emsg = (char *)get_pool_memory(PM_FNAME);
651             pool_mem = true;
652          }
653          Mmsg(emsg, _("is waiting for Client %s to connect to Storage %s"),
654               jcr->client->name(), jcr->wstore->name());
655          msg = emsg;
656          break;
657       }
658       switch (jcr->JobType) {
659       case JT_ADMIN:
660       case JT_RESTORE:
661          bstrncpy(level, "      ", sizeof(level));
662          break;
663       default:
664          bstrncpy(level, level_to_str(jcr->JobLevel), sizeof(level));
665          level[7] = 0;
666          break;
667       }
668
669       bsendmsg(ua, _("%6d %-6s  %-20s %s\n"),
670          jcr->JobId,
671          level,
672          jcr->Job,
673          msg);
674
675       if (pool_mem) {
676          free_pool_memory(emsg);
677          pool_mem = false;
678       }
679    }
680    endeach_jcr(jcr);
681    bsendmsg(ua, _("====\n"));
682    Dmsg0(200, "leave list_run_jobs()\n");
683 }
684
685 static void list_terminated_jobs(UAContext *ua)
686 {
687    char dt[MAX_TIME_LENGTH], b1[30], b2[30];
688    char level[10];
689
690    if (last_jobs->empty()) {
691       bsendmsg(ua, _("No Terminated Jobs.\n"));
692       return;
693    }
694    lock_last_jobs_list();
695    struct s_last_job *je;
696    bsendmsg(ua, _("\nTerminated Jobs:\n"));
697    bsendmsg(ua, _(" JobId  Level    Files      Bytes   Status   Finished        Name \n"));
698    bsendmsg(ua, _("====================================================================\n"));
699    foreach_dlist(je, last_jobs) {
700       char JobName[MAX_NAME_LENGTH];
701       const char *termstat;
702
703       bstrncpy(JobName, je->Job, sizeof(JobName));
704       /* There are three periods after the Job name */
705       char *p;
706       for (int i=0; i<3; i++) {
707          if ((p=strrchr(JobName, '.')) != NULL) {
708             *p = 0;
709          }
710       }
711
712       if (!acl_access_ok(ua, Job_ACL, JobName)) {
713          continue;
714       }
715
716       bstrftime_nc(dt, sizeof(dt), je->end_time);
717       switch (je->JobType) {
718       case JT_ADMIN:
719       case JT_RESTORE:
720          bstrncpy(level, "    ", sizeof(level));
721          break;
722       default:
723          bstrncpy(level, level_to_str(je->JobLevel), sizeof(level));
724          level[4] = 0;
725          break;
726       }
727       switch (je->JobStatus) {
728       case JS_Created:
729          termstat = _("Created");
730          break;
731       case JS_FatalError:
732       case JS_ErrorTerminated:
733          termstat = _("Error");
734          break;
735       case JS_Differences:
736          termstat = _("Diffs");
737          break;
738       case JS_Canceled:
739          termstat = _("Cancel");
740          break;
741       case JS_Terminated:
742          termstat = _("OK");
743          break;
744       default:
745          termstat = _("Other");
746          break;
747       }
748       bsendmsg(ua, _("%6d  %-6s %8s %10s  %-7s  %-8s %s\n"),
749          je->JobId,
750          level,
751          edit_uint64_with_commas(je->JobFiles, b1),
752          edit_uint64_with_suffix(je->JobBytes, b2),
753          termstat,
754          dt, JobName);
755    }
756    bsendmsg(ua, _("\n"));
757    unlock_last_jobs_list();
758 }