]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_status.c
kes Add DataDespooling and DataCommitting status (committing is
[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       case JS_DataCommitting:
640          msg = _("SD committing Data");
641          break;
642       case JS_DataDespooling:
643          msg = _("SD despooling Data");
644          break;
645       case JS_AttrDespooling:
646          msg = _("SD despooling Attributes");
647          break;
648       case JS_AttrInserting:
649          msg = _("Dir inserting Attributes");
650          break;
651
652       default:
653          emsg = (char *)get_pool_memory(PM_FNAME);
654          Mmsg(emsg, _("is in unknown state %c"), jcr->JobStatus);
655          pool_mem = true;
656          msg = emsg;
657          break;
658       }
659       /*
660        * Now report Storage daemon status code
661        */
662       switch (jcr->SDJobStatus) {
663       case JS_WaitMount:
664          if (pool_mem) {
665             free_pool_memory(emsg);
666             pool_mem = false;
667          }
668          msg = _("is waiting for a mount request");
669          break;
670       case JS_WaitMedia:
671          if (pool_mem) {
672             free_pool_memory(emsg);
673             pool_mem = false;
674          }
675          msg = _("is waiting for an appendable Volume");
676          break;
677       case JS_WaitFD:
678          if (!pool_mem) {
679             emsg = (char *)get_pool_memory(PM_FNAME);
680             pool_mem = true;
681          }
682          if (!jcr->client || !jcr->wstore) {
683             Mmsg(emsg, _("is waiting for Client to connect to Storage daemon"));
684          } else {
685             Mmsg(emsg, _("is waiting for Client %s to connect to Storage %s"),
686                  jcr->client->name(), jcr->wstore->name());
687         }
688         msg = emsg;
689         break;
690       case JS_DataCommitting:
691          msg = _("SD committing Data");
692          break;
693       case JS_DataDespooling:
694          msg = _("SD despooling Data");
695          break;
696       case JS_AttrDespooling:
697          msg = _("SD despooling Attributes");
698          break;
699       case JS_AttrInserting:
700          msg = _("Dir inserting Attributes");
701          break;
702       }
703       switch (jcr->JobType) {
704       case JT_ADMIN:
705       case JT_RESTORE:
706          bstrncpy(level, "      ", sizeof(level));
707          break;
708       default:
709          bstrncpy(level, level_to_str(jcr->JobLevel), sizeof(level));
710          level[7] = 0;
711          break;
712       }
713
714       ua->send_msg(_("%6d %-6s  %-20s %s\n"),
715          jcr->JobId,
716          level,
717          jcr->Job,
718          msg);
719
720       if (pool_mem) {
721          free_pool_memory(emsg);
722          pool_mem = false;
723       }
724    }
725    endeach_jcr(jcr);
726    ua->send_msg(_("====\n"));
727    Dmsg0(200, "leave list_run_jobs()\n");
728 }
729
730 static void list_terminated_jobs(UAContext *ua)
731 {
732    char dt[MAX_TIME_LENGTH], b1[30], b2[30];
733    char level[10];
734
735    if (last_jobs->empty()) {
736       ua->send_msg(_("No Terminated Jobs.\n"));
737       return;
738    }
739    lock_last_jobs_list();
740    struct s_last_job *je;
741    ua->send_msg(_("\nTerminated Jobs:\n"));
742    ua->send_msg(_(" JobId  Level    Files      Bytes   Status   Finished        Name \n"));
743    ua->send_msg(_("====================================================================\n"));
744    foreach_dlist(je, last_jobs) {
745       char JobName[MAX_NAME_LENGTH];
746       const char *termstat;
747
748       bstrncpy(JobName, je->Job, sizeof(JobName));
749       /* There are three periods after the Job name */
750       char *p;
751       for (int i=0; i<3; i++) {
752          if ((p=strrchr(JobName, '.')) != NULL) {
753             *p = 0;
754          }
755       }
756
757       if (!acl_access_ok(ua, Job_ACL, JobName)) {
758          continue;
759       }
760
761       bstrftime_nc(dt, sizeof(dt), je->end_time);
762       switch (je->JobType) {
763       case JT_ADMIN:
764       case JT_RESTORE:
765          bstrncpy(level, "    ", sizeof(level));
766          break;
767       default:
768          bstrncpy(level, level_to_str(je->JobLevel), sizeof(level));
769          level[4] = 0;
770          break;
771       }
772       switch (je->JobStatus) {
773       case JS_Created:
774          termstat = _("Created");
775          break;
776       case JS_FatalError:
777       case JS_ErrorTerminated:
778          termstat = _("Error");
779          break;
780       case JS_Differences:
781          termstat = _("Diffs");
782          break;
783       case JS_Canceled:
784          termstat = _("Cancel");
785          break;
786       case JS_Terminated:
787          termstat = _("OK");
788          break;
789       default:
790          termstat = _("Other");
791          break;
792       }
793       ua->send_msg(_("%6d  %-6s %8s %10s  %-7s  %-8s %s\n"),
794          je->JobId,
795          level,
796          edit_uint64_with_commas(je->JobFiles, b1),
797          edit_uint64_with_suffix(je->JobBytes, b2),
798          termstat,
799          dt, JobName);
800    }
801    ua->send_msg(_("\n"));
802    unlock_last_jobs_list();
803 }