]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_status.c
ebl Commit file relocation
[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       Dmsg1(250, "Using pool=%s\n", jcr->pool->name());
397       if (jcr->db) {
398          close_db = true;             /* new db opened, remember to close it */
399       }
400       if (ok) {
401          mr.PoolId = jcr->jr.PoolId;
402          mr.StorageId = sp->store->StorageId;
403          jcr->wstore = sp->store;
404          Dmsg0(250, "call find_next_volume_for_append\n");
405          ok = find_next_volume_for_append(jcr, &mr, 1, fnv_no_create_vol, fnv_no_prune);
406       }
407       if (!ok) {
408          bstrncpy(mr.VolumeName, "*unknown*", sizeof(mr.VolumeName));
409       }
410    }
411    bstrftime_nc(dt, sizeof(dt), sp->runtime);
412    switch (sp->job->JobType) {
413    case JT_ADMIN:
414    case JT_RESTORE:
415       level_ptr = " ";
416       break;
417    default:
418       level_ptr = level_to_str(sp->level);
419       break;
420    }
421    bsendmsg(ua, _("%-14s %-8s %3d  %-18s %-18s %s\n"),
422       level_ptr, job_type_to_str(sp->job->JobType), sp->priority, dt,
423       sp->job->name(), mr.VolumeName);
424    if (close_db) {
425       db_close_database(jcr, jcr->db);
426    }
427    jcr->db = ua->db;                  /* restore ua db to jcr */
428 }
429
430 /*
431  * Sort items by runtime, priority
432  */
433 static int my_compare(void *item1, void *item2)
434 {
435    sched_pkt *p1 = (sched_pkt *)item1;
436    sched_pkt *p2 = (sched_pkt *)item2;
437    if (p1->runtime < p2->runtime) {
438       return -1;
439    } else if (p1->runtime > p2->runtime) {
440       return 1;
441    }
442    if (p1->priority < p2->priority) {
443       return -1;
444    } else if (p1->priority > p2->priority) {
445       return 1;
446    }
447    return 0;
448 }
449
450 /*
451  * Find all jobs to be run in roughly the
452  *  next 24 hours.
453  */
454 static void list_scheduled_jobs(UAContext *ua)
455 {
456    time_t runtime;
457    RUN *run;
458    JOB *job;
459    int level, num_jobs = 0;
460    int priority;
461    bool hdr_printed = false;
462    dlist sched;
463    sched_pkt *sp;
464    int days, i;
465
466    Dmsg0(200, "enter list_sched_jobs()\n");
467
468    days = 1;
469    i = find_arg_with_value(ua, NT_("days"));
470    if (i >= 0) {
471      days = atoi(ua->argv[i]);
472      if ((days < 0) || (days > 50)) {
473        bsendmsg(ua, _("Ignoring invalid value for days. Max is 50.\n"));
474        days = 1;
475      }
476    }
477
478    /* Loop through all jobs */
479    LockRes();
480    foreach_res(job, R_JOB) {
481       if (!acl_access_ok(ua, Job_ACL, job->name()) || !job->enabled) {
482          continue;
483       }
484       for (run=NULL; (run = find_next_run(run, job, runtime, days)); ) {
485          USTORE store;
486          level = job->JobLevel;
487          if (run->level) {
488             level = run->level;
489          }
490          priority = job->Priority;
491          if (run->Priority) {
492             priority = run->Priority;
493          }
494          if (!hdr_printed) {
495             prt_runhdr(ua);
496             hdr_printed = true;
497          }
498          sp = (sched_pkt *)malloc(sizeof(sched_pkt));
499          sp->job = job;
500          sp->level = level;
501          sp->priority = priority;
502          sp->runtime = runtime;
503          sp->pool = run->pool;
504          get_job_storage(&store, job, run);
505          sp->store = store.store;
506          Dmsg3(250, "job=%s store=%s MediaType=%s\n", job->name(), sp->store->name(), sp->store->media_type);
507          sched.binary_insert_multiple(sp, my_compare);
508          num_jobs++;
509       }
510    } /* end for loop over resources */
511    UnlockRes();
512    foreach_dlist(sp, &sched) {
513       prt_runtime(ua, sp);
514    }
515    if (num_jobs == 0) {
516       bsendmsg(ua, _("No Scheduled Jobs.\n"));
517    }
518    bsendmsg(ua, _("====\n"));
519    Dmsg0(200, "Leave list_sched_jobs_runs()\n");
520 }
521
522 static void list_running_jobs(UAContext *ua)
523 {
524    JCR *jcr;
525    int njobs = 0;
526    const char *msg;
527    char *emsg;                        /* edited message */
528    char dt[MAX_TIME_LENGTH];
529    char level[10];
530    bool pool_mem = false;
531
532    Dmsg0(200, "enter list_run_jobs()\n");
533    bsendmsg(ua, _("\nRunning Jobs:\n"));
534    foreach_jcr(jcr) {
535       if (jcr->JobId == 0) {      /* this is us */
536          /* this is a console or other control job. We only show console
537           * jobs in the status output.
538           */
539          if (jcr->JobType == JT_CONSOLE) {
540             bstrftime_nc(dt, sizeof(dt), jcr->start_time);
541             bsendmsg(ua, _("Console connected at %s\n"), dt);
542          }
543          continue;
544       }       
545       njobs++;
546    }
547    endeach_jcr(jcr);
548
549    if (njobs == 0) {
550       /* Note the following message is used in regress -- don't change */
551       bsendmsg(ua, _("No Jobs running.\n====\n"));
552       Dmsg0(200, "leave list_run_jobs()\n");
553       return;
554    }
555    njobs = 0;
556    bsendmsg(ua, _(" JobId Level   Name                       Status\n"));
557    bsendmsg(ua, _("======================================================================\n"));
558    foreach_jcr(jcr) {
559       if (jcr->JobId == 0 || !acl_access_ok(ua, Job_ACL, jcr->job->name())) {
560          continue;
561       }
562       njobs++;
563       switch (jcr->JobStatus) {
564       case JS_Created:
565          msg = _("is waiting execution");
566          break;
567       case JS_Running:
568          msg = _("is running");
569          break;
570       case JS_Blocked:
571          msg = _("is blocked");
572          break;
573       case JS_Terminated:
574          msg = _("has terminated");
575          break;
576       case JS_ErrorTerminated:
577          msg = _("has erred");
578          break;
579       case JS_Error:
580          msg = _("has errors");
581          break;
582       case JS_FatalError:
583          msg = _("has a fatal error");
584          break;
585       case JS_Differences:
586          msg = _("has verify differences");
587          break;
588       case JS_Canceled:
589          msg = _("has been canceled");
590          break;
591       case JS_WaitFD:
592          emsg = (char *) get_pool_memory(PM_FNAME);
593          Mmsg(emsg, _("is waiting on Client %s"), jcr->client->name());
594          pool_mem = true;
595          msg = emsg;
596          break;
597       case JS_WaitSD:
598          emsg = (char *) get_pool_memory(PM_FNAME);
599          if (jcr->wstore) {
600             Mmsg(emsg, _("is waiting on Storage %s"), jcr->wstore->name());
601          } else {
602             Mmsg(emsg, _("is waiting on Storage %s"), jcr->rstore->name());
603          }
604          pool_mem = true;
605          msg = emsg;
606          break;
607       case JS_WaitStoreRes:
608          msg = _("is waiting on max Storage jobs");
609          break;
610       case JS_WaitClientRes:
611          msg = _("is waiting on max Client jobs");
612          break;
613       case JS_WaitJobRes:
614          msg = _("is waiting on max Job jobs");
615          break;
616       case JS_WaitMaxJobs:
617          msg = _("is waiting on max total jobs");
618          break;
619       case JS_WaitStartTime:
620          msg = _("is waiting for its start time");
621          break;
622       case JS_WaitPriority:
623          msg = _("is waiting for higher priority jobs to finish");
624          break;
625
626       default:
627          emsg = (char *) get_pool_memory(PM_FNAME);
628          Mmsg(emsg, _("is in unknown state %c"), jcr->JobStatus);
629          pool_mem = true;
630          msg = emsg;
631          break;
632       }
633       /*
634        * Now report Storage daemon status code
635        */
636       switch (jcr->SDJobStatus) {
637       case JS_WaitMount:
638          if (pool_mem) {
639             free_pool_memory(emsg);
640             pool_mem = false;
641          }
642          msg = _("is waiting for a mount request");
643          break;
644       case JS_WaitMedia:
645          if (pool_mem) {
646             free_pool_memory(emsg);
647             pool_mem = false;
648          }
649          msg = _("is waiting for an appendable Volume");
650          break;
651       case JS_WaitFD:
652          if (!pool_mem) {
653             emsg = (char *)get_pool_memory(PM_FNAME);
654             pool_mem = true;
655          }
656          Mmsg(emsg, _("is waiting for Client %s to connect to Storage %s"),
657               jcr->client->name(), jcr->wstore->name());
658          msg = emsg;
659          break;
660       }
661       switch (jcr->JobType) {
662       case JT_ADMIN:
663       case JT_RESTORE:
664          bstrncpy(level, "      ", sizeof(level));
665          break;
666       default:
667          bstrncpy(level, level_to_str(jcr->JobLevel), sizeof(level));
668          level[7] = 0;
669          break;
670       }
671
672       bsendmsg(ua, _("%6d %-6s  %-20s %s\n"),
673          jcr->JobId,
674          level,
675          jcr->Job,
676          msg);
677
678       if (pool_mem) {
679          free_pool_memory(emsg);
680          pool_mem = false;
681       }
682    }
683    endeach_jcr(jcr);
684    bsendmsg(ua, _("====\n"));
685    Dmsg0(200, "leave list_run_jobs()\n");
686 }
687
688 static void list_terminated_jobs(UAContext *ua)
689 {
690    char dt[MAX_TIME_LENGTH], b1[30], b2[30];
691    char level[10];
692
693    if (last_jobs->empty()) {
694       bsendmsg(ua, _("No Terminated Jobs.\n"));
695       return;
696    }
697    lock_last_jobs_list();
698    struct s_last_job *je;
699    bsendmsg(ua, _("\nTerminated Jobs:\n"));
700    bsendmsg(ua, _(" JobId  Level    Files      Bytes   Status   Finished        Name \n"));
701    bsendmsg(ua, _("====================================================================\n"));
702    foreach_dlist(je, last_jobs) {
703       char JobName[MAX_NAME_LENGTH];
704       const char *termstat;
705
706       bstrncpy(JobName, je->Job, sizeof(JobName));
707       /* There are three periods after the Job name */
708       char *p;
709       for (int i=0; i<3; i++) {
710          if ((p=strrchr(JobName, '.')) != NULL) {
711             *p = 0;
712          }
713       }
714
715       if (!acl_access_ok(ua, Job_ACL, JobName)) {
716          continue;
717       }
718
719       bstrftime_nc(dt, sizeof(dt), je->end_time);
720       switch (je->JobType) {
721       case JT_ADMIN:
722       case JT_RESTORE:
723          bstrncpy(level, "    ", sizeof(level));
724          break;
725       default:
726          bstrncpy(level, level_to_str(je->JobLevel), sizeof(level));
727          level[4] = 0;
728          break;
729       }
730       switch (je->JobStatus) {
731       case JS_Created:
732          termstat = _("Created");
733          break;
734       case JS_FatalError:
735       case JS_ErrorTerminated:
736          termstat = _("Error");
737          break;
738       case JS_Differences:
739          termstat = _("Diffs");
740          break;
741       case JS_Canceled:
742          termstat = _("Cancel");
743          break;
744       case JS_Terminated:
745          termstat = _("OK");
746          break;
747       default:
748          termstat = _("Other");
749          break;
750       }
751       bsendmsg(ua, _("%6d  %-6s %8s %10s  %-7s  %-8s %s\n"),
752          je->JobId,
753          level,
754          edit_uint64_with_commas(je->JobFiles, b1),
755          edit_uint64_with_suffix(je->JobBytes, b2),
756          termstat,
757          dt, JobName);
758    }
759    bsendmsg(ua, _("\n"));
760    unlock_last_jobs_list();
761 }