]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_status.c
7b264d28cd9224b38ac89ba7d0fb5d2154bc9354
[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       ua->send_msg("1900 Bad .status command, missing arguments.\n");
66       return false;
67    }
68
69    if (strcasecmp(ua->argk[2], "current") == 0) {
70       ua->send_msg(OKqstatus, ua->argk[2]);
71       foreach_jcr(njcr) {
72          if (njcr->JobId != 0 && acl_access_ok(ua, Job_ACL, njcr->job->name())) {
73             ua->send_msg(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       ua->send_msg(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             ua->send_msg(DotStatusJob, edit_int64(job->JobId, ed1), 
84                   job->JobStatus, job->Errors);
85          }
86       }
87    } else {
88       ua->send_msg("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 void list_dir_status_header(UAContext *ua)
257 {
258    char dt[MAX_TIME_LENGTH];
259    char b1[35], b2[35], b3[35], b4[35];
260
261    ua->send_msg(_("%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       ua->send_msg(_("Daemon started %s, 1 Job run since started.\n"), dt);
266    }
267    else {
268       ua->send_msg(_("Daemon started %s, %d Jobs run since started.\n"),
269         dt, num_jobs_run);
270    }
271    ua->send_msg(_(" 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 static void do_director_status(UAContext *ua)
279 {
280    list_dir_status_header(ua);
281
282    /*
283     * List scheduled Jobs
284     */
285    list_scheduled_jobs(ua);
286
287    /*
288     * List running jobs
289     */
290    list_running_jobs(ua);
291
292    /*
293     * List terminated jobs
294     */
295    list_terminated_jobs(ua);
296    ua->send_msg(_("====\n"));
297 }
298
299 static void do_storage_status(UAContext *ua, STORE *store)
300 {
301    BSOCK *sd;
302    USTORE lstore;
303
304    lstore.store = store;
305    pm_strcpy(lstore.store_source, _("unknown source"));
306    set_wstorage(ua->jcr, &lstore);
307    /* Try connecting for up to 15 seconds */
308    ua->send_msg(_("Connecting to Storage daemon %s at %s:%d\n"),
309       store->name(), store->address, store->SDport);
310    if (!connect_to_storage_daemon(ua->jcr, 1, 15, 0)) {
311       ua->send_msg(_("\nFailed to connect to Storage daemon %s.\n====\n"),
312          store->name());
313       if (ua->jcr->store_bsock) {
314          bnet_close(ua->jcr->store_bsock);
315          ua->jcr->store_bsock = NULL;
316       }
317       return;
318    }
319    Dmsg0(20, _("Connected to storage daemon\n"));
320    sd = ua->jcr->store_bsock;
321    bnet_fsend(sd, "status");
322    while (bnet_recv(sd) >= 0) {
323       ua->send_msg("%s", sd->msg);
324    }
325    bnet_sig(sd, BNET_TERMINATE);
326    bnet_close(sd);
327    ua->jcr->store_bsock = NULL;
328    return;
329 }
330
331 static void do_client_status(UAContext *ua, CLIENT *client)
332 {
333    BSOCK *fd;
334
335    /* Connect to File daemon */
336
337    ua->jcr->client = client;
338    /* Release any old dummy key */
339    if (ua->jcr->sd_auth_key) {
340       free(ua->jcr->sd_auth_key);
341    }
342    /* Create a new dummy SD auth key */
343    ua->jcr->sd_auth_key = bstrdup("dummy");
344
345    /* Try to connect for 15 seconds */
346    ua->send_msg(_("Connecting to Client %s at %s:%d\n"),
347       client->name(), client->address, client->FDport);
348    if (!connect_to_file_daemon(ua->jcr, 1, 15, 0)) {
349       ua->send_msg(_("Failed to connect to Client %s.\n====\n"),
350          client->name());
351       if (ua->jcr->file_bsock) {
352          bnet_close(ua->jcr->file_bsock);
353          ua->jcr->file_bsock = NULL;
354       }
355       return;
356    }
357    Dmsg0(20, _("Connected to file daemon\n"));
358    fd = ua->jcr->file_bsock;
359    bnet_fsend(fd, "status");
360    while (bnet_recv(fd) >= 0) {
361       ua->send_msg("%s", fd->msg);
362    }
363    bnet_sig(fd, BNET_TERMINATE);
364    bnet_close(fd);
365    ua->jcr->file_bsock = NULL;
366
367    return;
368 }
369
370 static void prt_runhdr(UAContext *ua)
371 {
372    ua->send_msg(_("\nScheduled Jobs:\n"));
373    ua->send_msg(_("Level          Type     Pri  Scheduled          Name               Volume\n"));
374    ua->send_msg(_("===================================================================================\n"));
375 }
376
377 /* Scheduling packet */
378 struct sched_pkt {
379    dlink link;                        /* keep this as first item!!! */
380    JOB *job;
381    int level;
382    int priority;
383    time_t runtime;
384    POOL *pool;
385    STORE *store;
386 };
387
388 static void prt_runtime(UAContext *ua, sched_pkt *sp)
389 {
390    char dt[MAX_TIME_LENGTH];
391    const char *level_ptr;
392    bool ok = false;
393    bool close_db = false;
394    JCR *jcr = ua->jcr;
395    MEDIA_DBR mr;
396
397    memset(&mr, 0, sizeof(mr));
398    if (sp->job->JobType == JT_BACKUP) {
399       jcr->db = NULL;
400       ok = complete_jcr_for_job(jcr, sp->job, sp->pool);
401       Dmsg1(250, "Using pool=%s\n", jcr->pool->name());
402       if (jcr->db) {
403          close_db = true;             /* new db opened, remember to close it */
404       }
405       if (ok) {
406          mr.PoolId = jcr->jr.PoolId;
407          mr.StorageId = sp->store->StorageId;
408          jcr->wstore = sp->store;
409          Dmsg0(250, "call find_next_volume_for_append\n");
410          ok = find_next_volume_for_append(jcr, &mr, 1, fnv_no_create_vol, fnv_no_prune);
411       }
412       if (!ok) {
413          bstrncpy(mr.VolumeName, "*unknown*", sizeof(mr.VolumeName));
414       }
415    }
416    bstrftime_nc(dt, sizeof(dt), sp->runtime);
417    switch (sp->job->JobType) {
418    case JT_ADMIN:
419    case JT_RESTORE:
420       level_ptr = " ";
421       break;
422    default:
423       level_ptr = level_to_str(sp->level);
424       break;
425    }
426    ua->send_msg(_("%-14s %-8s %3d  %-18s %-18s %s\n"),
427       level_ptr, job_type_to_str(sp->job->JobType), sp->priority, dt,
428       sp->job->name(), mr.VolumeName);
429    if (close_db) {
430       db_close_database(jcr, jcr->db);
431    }
432    jcr->db = ua->db;                  /* restore ua db to jcr */
433 }
434
435 /*
436  * Sort items by runtime, priority
437  */
438 static int my_compare(void *item1, void *item2)
439 {
440    sched_pkt *p1 = (sched_pkt *)item1;
441    sched_pkt *p2 = (sched_pkt *)item2;
442    if (p1->runtime < p2->runtime) {
443       return -1;
444    } else if (p1->runtime > p2->runtime) {
445       return 1;
446    }
447    if (p1->priority < p2->priority) {
448       return -1;
449    } else if (p1->priority > p2->priority) {
450       return 1;
451    }
452    return 0;
453 }
454
455 /*
456  * Find all jobs to be run in roughly the
457  *  next 24 hours.
458  */
459 static void list_scheduled_jobs(UAContext *ua)
460 {
461    time_t runtime;
462    RUN *run;
463    JOB *job;
464    int level, num_jobs = 0;
465    int priority;
466    bool hdr_printed = false;
467    dlist sched;
468    sched_pkt *sp;
469    int days, i;
470
471    Dmsg0(200, "enter list_sched_jobs()\n");
472
473    days = 1;
474    i = find_arg_with_value(ua, NT_("days"));
475    if (i >= 0) {
476      days = atoi(ua->argv[i]);
477      if ((days < 0) || (days > 50)) {
478        ua->send_msg(_("Ignoring invalid value for days. Max is 50.\n"));
479        days = 1;
480      }
481    }
482
483    /* Loop through all jobs */
484    LockRes();
485    foreach_res(job, R_JOB) {
486       if (!acl_access_ok(ua, Job_ACL, job->name()) || !job->enabled) {
487          continue;
488       }
489       for (run=NULL; (run = find_next_run(run, job, runtime, days)); ) {
490          USTORE store;
491          level = job->JobLevel;
492          if (run->level) {
493             level = run->level;
494          }
495          priority = job->Priority;
496          if (run->Priority) {
497             priority = run->Priority;
498          }
499          if (!hdr_printed) {
500             prt_runhdr(ua);
501             hdr_printed = true;
502          }
503          sp = (sched_pkt *)malloc(sizeof(sched_pkt));
504          sp->job = job;
505          sp->level = level;
506          sp->priority = priority;
507          sp->runtime = runtime;
508          sp->pool = run->pool;
509          get_job_storage(&store, job, run);
510          sp->store = store.store;
511          Dmsg3(250, "job=%s store=%s MediaType=%s\n", job->name(), sp->store->name(), sp->store->media_type);
512          sched.binary_insert_multiple(sp, my_compare);
513          num_jobs++;
514       }
515    } /* end for loop over resources */
516    UnlockRes();
517    foreach_dlist(sp, &sched) {
518       prt_runtime(ua, sp);
519    }
520    if (num_jobs == 0) {
521       ua->send_msg(_("No Scheduled Jobs.\n"));
522    }
523    ua->send_msg(_("====\n"));
524    Dmsg0(200, "Leave list_sched_jobs_runs()\n");
525 }
526
527 static void list_running_jobs(UAContext *ua)
528 {
529    JCR *jcr;
530    int njobs = 0;
531    const char *msg;
532    char *emsg;                        /* edited message */
533    char dt[MAX_TIME_LENGTH];
534    char level[10];
535    bool pool_mem = false;
536
537    Dmsg0(200, "enter list_run_jobs()\n");
538    ua->send_msg(_("\nRunning Jobs:\n"));
539    foreach_jcr(jcr) {
540       if (jcr->JobId == 0) {      /* this is us */
541          /* this is a console or other control job. We only show console
542           * jobs in the status output.
543           */
544          if (jcr->JobType == JT_CONSOLE) {
545             bstrftime_nc(dt, sizeof(dt), jcr->start_time);
546             ua->send_msg(_("Console connected at %s\n"), dt);
547          }
548          continue;
549       }       
550       njobs++;
551    }
552    endeach_jcr(jcr);
553
554    if (njobs == 0) {
555       /* Note the following message is used in regress -- don't change */
556       ua->send_msg(_("No Jobs running.\n====\n"));
557       Dmsg0(200, "leave list_run_jobs()\n");
558       return;
559    }
560    njobs = 0;
561    ua->send_msg(_(" JobId Level   Name                       Status\n"));
562    ua->send_msg(_("======================================================================\n"));
563    foreach_jcr(jcr) {
564       if (jcr->JobId == 0 || !acl_access_ok(ua, Job_ACL, jcr->job->name())) {
565          continue;
566       }
567       njobs++;
568       switch (jcr->JobStatus) {
569       case JS_Created:
570          msg = _("is waiting execution");
571          break;
572       case JS_Running:
573          msg = _("is running");
574          break;
575       case JS_Blocked:
576          msg = _("is blocked");
577          break;
578       case JS_Terminated:
579          msg = _("has terminated");
580          break;
581       case JS_ErrorTerminated:
582          msg = _("has erred");
583          break;
584       case JS_Error:
585          msg = _("has errors");
586          break;
587       case JS_FatalError:
588          msg = _("has a fatal error");
589          break;
590       case JS_Differences:
591          msg = _("has verify differences");
592          break;
593       case JS_Canceled:
594          msg = _("has been canceled");
595          break;
596       case JS_WaitFD:
597          emsg = (char *) get_pool_memory(PM_FNAME);
598          Mmsg(emsg, _("is waiting on Client %s"), jcr->client->name());
599          pool_mem = true;
600          msg = emsg;
601          break;
602       case JS_WaitSD:
603          emsg = (char *) get_pool_memory(PM_FNAME);
604          if (jcr->wstore) {
605             Mmsg(emsg, _("is waiting on Storage %s"), jcr->wstore->name());
606          } else {
607             Mmsg(emsg, _("is waiting on Storage %s"), jcr->rstore->name());
608          }
609          pool_mem = true;
610          msg = emsg;
611          break;
612       case JS_WaitStoreRes:
613          msg = _("is waiting on max Storage jobs");
614          break;
615       case JS_WaitClientRes:
616          msg = _("is waiting on max Client jobs");
617          break;
618       case JS_WaitJobRes:
619          msg = _("is waiting on max Job jobs");
620          break;
621       case JS_WaitMaxJobs:
622          msg = _("is waiting on max total jobs");
623          break;
624       case JS_WaitStartTime:
625          msg = _("is waiting for its start time");
626          break;
627       case JS_WaitPriority:
628          msg = _("is waiting for higher priority jobs to finish");
629          break;
630
631       default:
632          emsg = (char *) get_pool_memory(PM_FNAME);
633          Mmsg(emsg, _("is in unknown state %c"), jcr->JobStatus);
634          pool_mem = true;
635          msg = emsg;
636          break;
637       }
638       /*
639        * Now report Storage daemon status code
640        */
641       switch (jcr->SDJobStatus) {
642       case JS_WaitMount:
643          if (pool_mem) {
644             free_pool_memory(emsg);
645             pool_mem = false;
646          }
647          msg = _("is waiting for a mount request");
648          break;
649       case JS_WaitMedia:
650          if (pool_mem) {
651             free_pool_memory(emsg);
652             pool_mem = false;
653          }
654          msg = _("is waiting for an appendable Volume");
655          break;
656       case JS_WaitFD:
657          if (!pool_mem) {
658             emsg = (char *)get_pool_memory(PM_FNAME);
659             pool_mem = true;
660          }
661          Mmsg(emsg, _("is waiting for Client %s to connect to Storage %s"),
662               jcr->client->name(), jcr->wstore->name());
663          msg = emsg;
664          break;
665       }
666       switch (jcr->JobType) {
667       case JT_ADMIN:
668       case JT_RESTORE:
669          bstrncpy(level, "      ", sizeof(level));
670          break;
671       default:
672          bstrncpy(level, level_to_str(jcr->JobLevel), sizeof(level));
673          level[7] = 0;
674          break;
675       }
676
677       ua->send_msg(_("%6d %-6s  %-20s %s\n"),
678          jcr->JobId,
679          level,
680          jcr->Job,
681          msg);
682
683       if (pool_mem) {
684          free_pool_memory(emsg);
685          pool_mem = false;
686       }
687    }
688    endeach_jcr(jcr);
689    ua->send_msg(_("====\n"));
690    Dmsg0(200, "leave list_run_jobs()\n");
691 }
692
693 static void list_terminated_jobs(UAContext *ua)
694 {
695    char dt[MAX_TIME_LENGTH], b1[30], b2[30];
696    char level[10];
697
698    if (last_jobs->empty()) {
699       ua->send_msg(_("No Terminated Jobs.\n"));
700       return;
701    }
702    lock_last_jobs_list();
703    struct s_last_job *je;
704    ua->send_msg(_("\nTerminated Jobs:\n"));
705    ua->send_msg(_(" JobId  Level    Files      Bytes   Status   Finished        Name \n"));
706    ua->send_msg(_("====================================================================\n"));
707    foreach_dlist(je, last_jobs) {
708       char JobName[MAX_NAME_LENGTH];
709       const char *termstat;
710
711       bstrncpy(JobName, je->Job, sizeof(JobName));
712       /* There are three periods after the Job name */
713       char *p;
714       for (int i=0; i<3; i++) {
715          if ((p=strrchr(JobName, '.')) != NULL) {
716             *p = 0;
717          }
718       }
719
720       if (!acl_access_ok(ua, Job_ACL, JobName)) {
721          continue;
722       }
723
724       bstrftime_nc(dt, sizeof(dt), je->end_time);
725       switch (je->JobType) {
726       case JT_ADMIN:
727       case JT_RESTORE:
728          bstrncpy(level, "    ", sizeof(level));
729          break;
730       default:
731          bstrncpy(level, level_to_str(je->JobLevel), sizeof(level));
732          level[4] = 0;
733          break;
734       }
735       switch (je->JobStatus) {
736       case JS_Created:
737          termstat = _("Created");
738          break;
739       case JS_FatalError:
740       case JS_ErrorTerminated:
741          termstat = _("Error");
742          break;
743       case JS_Differences:
744          termstat = _("Diffs");
745          break;
746       case JS_Canceled:
747          termstat = _("Cancel");
748          break;
749       case JS_Terminated:
750          termstat = _("OK");
751          break;
752       default:
753          termstat = _("Other");
754          break;
755       }
756       ua->send_msg(_("%6d  %-6s %8s %10s  %-7s  %-8s %s\n"),
757          je->JobId,
758          level,
759          edit_uint64_with_commas(je->JobFiles, b1),
760          edit_uint64_with_suffix(je->JobBytes, b2),
761          termstat,
762          dt, JobName);
763    }
764    ua->send_msg(_("\n"));
765    unlock_last_jobs_list();
766 }