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