2 Bacula® - The Network Backup Solution
4 Copyright (C) 2007-2008 Free Software Foundation Europe e.V.
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, which is
11 listed in the file LICENSE.
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.
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
23 Bacula® is a registered trademark of Kern Sibbald.
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.
29 * Main program to test loading and running Bacula plugins.
30 * Destined to become Bacula pluginloader, ...
32 * Kern Sibbald, October 2007
37 const int dbglvl = 150;
39 const char *plugin_type = "-fd.dll";
41 const char *plugin_type = "-fd.so";
44 extern int save_file(JCR *jcr, FF_PKT *ff_pkt, bool top_level);
46 /* Function pointers to be set here */
47 extern DLL_IMP_EXP int (*plugin_bopen)(BFILE *bfd, const char *fname, int flags, mode_t mode);
48 extern DLL_IMP_EXP int (*plugin_bclose)(BFILE *bfd);
49 extern DLL_IMP_EXP ssize_t (*plugin_bread)(BFILE *bfd, void *buf, size_t count);
50 extern DLL_IMP_EXP ssize_t (*plugin_bwrite)(BFILE *bfd, void *buf, size_t count);
51 extern DLL_IMP_EXP boffset_t (*plugin_blseek)(BFILE *bfd, boffset_t offset, int whence);
54 /* Forward referenced functions */
55 static bRC baculaGetValue(bpContext *ctx, bVariable var, void *value);
56 static bRC baculaSetValue(bpContext *ctx, bVariable var, void *value);
57 static bRC baculaRegisterEvents(bpContext *ctx, ...);
58 static bRC baculaJobMsg(bpContext *ctx, const char *file, int line,
59 int type, utime_t mtime, const char *fmt, ...);
60 static bRC baculaDebugMsg(bpContext *ctx, const char *file, int line,
61 int level, const char *fmt, ...);
62 static void *baculaMalloc(bpContext *ctx, const char *file, int line,
64 static void baculaFree(bpContext *ctx, const char *file, int line, void *mem);
67 * These will be plugged into the global pointer structure for
70 static int my_plugin_bopen(BFILE *bfd, const char *fname, int flags, mode_t mode);
71 static int my_plugin_bclose(BFILE *bfd);
72 static ssize_t my_plugin_bread(BFILE *bfd, void *buf, size_t count);
73 static ssize_t my_plugin_bwrite(BFILE *bfd, void *buf, size_t count);
74 static boffset_t my_plugin_blseek(BFILE *bfd, boffset_t offset, int whence);
78 static bInfo binfo = {
80 FD_PLUGIN_INTERFACE_VERSION
83 /* Bacula entry points */
84 static bFuncs bfuncs = {
86 FD_PLUGIN_INTERFACE_VERSION,
97 * Bacula private context
100 JCR *jcr; /* jcr for plugin */
101 bRC rc; /* last return code */
102 bool disabled; /* set if plugin disabled */
105 static bool is_plugin_disabled(JCR *jcr)
108 if (!jcr->plugin_ctx) {
111 b_ctx = (bacula_ctx *)jcr->plugin_ctx->bContext;
112 return b_ctx->disabled;
117 * Create a plugin event
119 void generate_plugin_event(JCR *jcr, bEventType eventType, void *value)
125 if (!plugin_list || !jcr || !jcr->plugin_ctx_list) {
126 return; /* Return if no plugins loaded */
129 bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list;
130 event.eventType = eventType;
132 Dmsg2(dbglvl, "plugin_ctx=%p JobId=%d\n", jcr->plugin_ctx_list, jcr->JobId);
134 /* Pass event to every plugin */
135 foreach_alist(plugin, plugin_list) {
137 jcr->plugin_ctx = &plugin_ctx_list[i++];
138 jcr->plugin = plugin;
139 if (is_plugin_disabled(jcr)) {
142 rc = plug_func(plugin)->handlePluginEvent(jcr->plugin_ctx, &event, value);
149 jcr->plugin_ctx = NULL;
154 * Sequence of calls for a backup:
155 * 1. plugin_save() here is called with ff_pkt
156 * 2. we find the plugin requested on the command string
157 * 3. we generate a bEventBackupCommand event to the specified plugin
158 * and pass it the command string.
159 * 4. we make a startPluginBackup call to the plugin, which gives
160 * us the data we need in save_pkt
161 * 5. we call Bacula's save_file() subroutine to save the specified
162 * file. The plugin will be called at pluginIO() to supply the
165 * Sequence of calls for restore:
166 * See subroutine plugin_name_stream() below.
168 int plugin_save(JCR *jcr, FF_PKT *ff_pkt, bool top_level)
174 char *cmd = ff_pkt->top_fname;
178 if (!plugin_list || !jcr->plugin_ctx_list) {
179 return 1; /* Return if no plugins loaded */
182 jcr->cmd_plugin = true;
183 bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list;
184 event.eventType = bEventBackupCommand;
186 /* Handle plugin command here backup */
187 Dmsg1(dbglvl, "plugin cmd=%s\n", cmd);
188 if (!(p = strchr(cmd, ':'))) {
189 Jmsg1(jcr, M_ERROR, 0, "Malformed plugin command: %s\n", cmd);
197 /* Note, we stop the loop on the first plugin that matches the name */
198 foreach_alist(plugin, plugin_list) {
199 Dmsg3(dbglvl, "plugin=%s cmd=%s len=%d\n", plugin->file, cmd, len);
200 if (strncmp(plugin->file, cmd, len) != 0) {
205 * We put the current plugin pointer, and the plugin context
206 * into the jcr, because during save_file(), the plugin
207 * will be called many times and these values are needed.
209 jcr->plugin_ctx = &plugin_ctx_list[i];
210 jcr->plugin = plugin;
211 if (is_plugin_disabled(jcr)) {
215 Dmsg1(dbglvl, "Command plugin = %s\n", cmd);
216 /* Send the backup command to the right plugin*/
217 if (plug_func(plugin)->handlePluginEvent(jcr->plugin_ctx, &event, cmd) != bRC_OK) {
220 /* Loop getting filenames to backup then saving them */
221 while (!job_canceled(jcr)) {
222 memset(&sp, 0, sizeof(sp));
223 sp.pkt_size = sizeof(sp);
224 sp.pkt_end = sizeof(sp);
227 Dmsg3(dbglvl, "startBackup st_size=%p st_blocks=%p sp=%p\n", &sp.statp.st_size, &sp.statp.st_blocks,
229 /* Get the file save parameters */
230 if (plug_func(plugin)->startBackupFile(jcr->plugin_ctx, &sp) != bRC_OK) {
233 if (sp.type == 0 || sp.fname == NULL) {
234 Jmsg1(jcr, M_FATAL, 0, _("Command plugin \"%s\" returned bad startBackupFile packet.\n"),
238 jcr->plugin_sp = &sp;
240 ff_pkt->fname = sp.fname;
241 ff_pkt->link = sp.link;
242 ff_pkt->type = sp.type;
243 memcpy(&ff_pkt->statp, &sp.statp, sizeof(ff_pkt->statp));
244 Dmsg1(dbglvl, "Save_file: file=%s\n", ff_pkt->fname);
245 save_file(jcr, ff_pkt, true);
246 bRC rc = plug_func(plugin)->endBackupFile(jcr->plugin_ctx);
247 if (rc == bRC_More) {
254 Jmsg1(jcr, M_ERROR, 0, "Command plugin \"%s\" not found.\n", cmd);
257 jcr->cmd_plugin = false;
259 jcr->plugin_ctx = NULL;
264 * Send plugin name start/end record to SD
266 bool send_plugin_name(JCR *jcr, BSOCK *sd, bool start)
269 int index = jcr->JobFiles;
270 struct save_pkt *sp = (struct save_pkt *)jcr->plugin_sp;
273 Jmsg0(jcr, M_FATAL, 0, _("Plugin save packet not found.\n"));
278 index++; /* JobFiles not incremented yet */
280 Dmsg1(dbglvl, "send_plugin_name=%s\n", sp->cmd);
281 /* Send stream header */
282 if (!sd->fsend("%ld %d 0", index, STREAM_PLUGIN_NAME)) {
283 Jmsg1(jcr, M_FATAL, 0, _("Network send error to SD. ERR=%s\n"),
287 Dmsg1(50, "send: %s\n", sd->msg);
290 /* Send data -- not much */
291 stat = sd->fsend("%ld 1 %d %s%c", index, sp->portable, sp->cmd, 0);
293 /* Send end of data */
294 stat = sd->fsend("0 0");
297 Jmsg1(jcr, M_FATAL, 0, _("Network send error to SD. ERR=%s\n"),
301 Dmsg1(dbglvl, "send: %s\n", sd->msg);
302 sd->signal(BNET_EOD); /* indicate end of plugin name data */
307 * Plugin name stream found during restore. The record passed in
308 * argument name was generated in send_plugin_name() above.
310 * Returns: true if start of stream
311 * false if end of steam
313 bool plugin_name_stream(JCR *jcr, char *name)
317 bool start, portable;
321 bpContext *plugin_ctx_list = jcr->plugin_ctx_list;
323 Dmsg1(dbglvl, "Read plugin stream string=%s\n", name);
324 skip_nonspaces(&p); /* skip over jcr->JobFiles */
328 /* Start of plugin data */
329 skip_nonspaces(&p); /* skip start/end flag */
331 portable = *p == '1';
332 skip_nonspaces(&p); /* skip portable flag */
337 * End of plugin data, notify plugin, then clear flags
339 Dmsg2(dbglvl, "End plugin data plugin=%p ctx=%p\n", jcr->plugin, jcr->plugin_ctx);
341 plug_func(jcr->plugin)->endRestoreFile(jcr->plugin_ctx);
343 jcr->plugin_ctx = NULL;
347 if (!plugin_ctx_list) {
352 * After this point, we are dealing with a restore start
355 // Dmsg1(dbglvl, "plugin restore cmd=%s\n", cmd);
356 if (!(p = strchr(cmd, ':'))) {
357 Jmsg1(jcr, M_ERROR, 0,
358 _("Malformed plugin command. Name not terminated by colon: %s\n"), cmd);
367 * Search for correct plugin as specified on the command
369 foreach_alist(plugin, plugin_list) {
371 Dmsg3(dbglvl, "plugin=%s cmd=%s len=%d\n", plugin->file, cmd, len);
372 if (strncmp(plugin->file, cmd, len) != 0) {
376 jcr->plugin_ctx = &plugin_ctx_list[i];
377 jcr->plugin = plugin;
378 if (is_plugin_disabled(jcr)) {
381 Dmsg1(dbglvl, "Restore Command plugin = %s\n", cmd);
382 event.eventType = bEventRestoreCommand;
383 if (plug_func(plugin)->handlePluginEvent(jcr->plugin_ctx,
384 &event, cmd) != bRC_OK) {
387 /* ***FIXME**** check error code */
388 plug_func(plugin)->startRestoreFile((bpContext *)jcr->plugin_ctx, cmd);
391 Jmsg1(jcr, M_WARNING, 0, _("Plugin=%s not found.\n"), cmd);
398 * Tell the plugin to create the file. Return values are
401 * CF_SKIP -- skip processing this file
402 * CF_EXTRACT -- extract the file (i.e.call i/o routines)
403 * CF_CREATED -- created, but no content to extract (typically directories)
406 int plugin_create_file(JCR *jcr, ATTR *attr, BFILE *bfd, int replace)
408 bpContext *plugin_ctx = jcr->plugin_ctx;
409 Plugin *plugin = jcr->plugin;
410 struct restore_pkt rp;
414 if (!plugin || !plugin_ctx || !set_cmd_plugin(bfd, jcr)) {
417 rp.pkt_size = sizeof(rp);
418 rp.pkt_end = sizeof(rp);
419 rp.stream = attr->stream;
420 rp.data_stream = attr->data_stream;
421 rp.type = attr->type;
422 rp.file_index = attr->file_index;
423 rp.LinkFI = attr->LinkFI;
425 rp.statp = attr->statp; /* structure assignment */
426 rp.attrEx = attr->attrEx;
427 rp.ofname = attr->ofname;
428 rp.olname = attr->olname;
429 rp.where = jcr->where;
430 rp.RegexWhere = jcr->RegexWhere;
431 rp.replace = jcr->replace;
432 rp.create_status = CF_ERROR;
433 Dmsg1(dbglvl, "call plugin createFile=%s\n", rp.ofname);
434 rc = plug_func(plugin)->createFile(plugin_ctx, &rp);
436 Qmsg2(jcr, M_ERROR, 0, _("Plugin createFile call failed. Stat=%d file=%s\n"),
440 if (rp.create_status == CF_ERROR) {
441 Qmsg1(jcr, M_ERROR, 0, _("Plugin createFile call failed. Returned CF_ERROR file=%s\n"),
445 /* Created link or directory? */
446 if (rp.create_status == CF_CREATED) {
447 return rp.create_status; /* yes, no need to bopen */
450 flags = O_WRONLY | O_CREAT | O_TRUNC | O_BINARY;
451 Dmsg0(dbglvl, "call bopen\n");
452 int stat = bopen(bfd, attr->ofname, flags, S_IRUSR | S_IWUSR);
453 Dmsg1(50, "bopen status=%d\n", stat);
456 be.set_errno(bfd->berrno);
457 Qmsg2(jcr, M_ERROR, 0, _("Could not create %s: ERR=%s\n"),
458 attr->ofname, be.bstrerror());
459 Dmsg2(dbglvl,"Could not bopen file %s: ERR=%s\n", attr->ofname, be.bstrerror());
463 if (!is_bopen(bfd)) {
464 Dmsg0(000, "===== BFD is not open!!!!\n");
470 * Reset the file attributes after all file I/O is done -- this allows
471 * the previous access time/dates to be set properly, and it also allows
472 * us to properly set directory permissions.
474 bool plugin_set_attributes(JCR *jcr, ATTR *attr, BFILE *ofd)
476 Dmsg0(dbglvl, "plugin_set_attributes\n");
480 pm_strcpy(attr->ofname, "*none*");
484 void dump_fd_plugin(Plugin *plugin, FILE *fp)
489 pInfo *info = (pInfo *) plugin->pinfo;
490 fprintf(fp, "\tversion=%d\n", info->version);
491 fprintf(fp, "\tdate=%s\n", NPRTB(info->plugin_date));
492 fprintf(fp, "\tmagic=%s\n", NPRTB(info->plugin_magic));
493 fprintf(fp, "\tauthor=%s\n", NPRTB(info->plugin_author));
494 fprintf(fp, "\tlicence=%s\n", NPRTB(info->plugin_license));
495 fprintf(fp, "\tversion=%s\n", NPRTB(info->plugin_version));
496 fprintf(fp, "\tdescription=%s\n", NPRTB(info->plugin_description));
500 * This entry point is called internally by Bacula to ensure
501 * that the plugin IO calls come into this code.
503 void load_fd_plugins(const char *plugin_dir)
508 Dmsg0(dbglvl, "plugin dir is NULL\n");
512 plugin_list = New(alist(10, not_owned_by_alist));
513 if (!load_plugins((void *)&binfo, (void *)&bfuncs, plugin_dir, plugin_type)) {
514 /* Either none found, or some error */
515 if (plugin_list->size() == 0) {
518 Dmsg0(dbglvl, "No plugins loaded\n");
523 /* Plug entry points called from findlib */
524 plugin_bopen = my_plugin_bopen;
525 plugin_bclose = my_plugin_bclose;
526 plugin_bread = my_plugin_bread;
527 plugin_bwrite = my_plugin_bwrite;
528 plugin_blseek = my_plugin_blseek;
529 foreach_alist(plugin, plugin_list) {
530 Jmsg(NULL, M_INFO, 0, _("Loaded plugin: %s\n"), plugin->file);
531 Dmsg1(dbglvl, "Loaded plugin: %s\n", plugin->file);
535 dbg_plugin_add_hook(dump_fd_plugin);
539 * Create a new instance of each plugin for this Job
540 * Note, plugin_list can exist but jcr->plugin_ctx_list can
541 * be NULL if no plugins were loaded.
543 void new_plugins(JCR *jcr)
549 Dmsg0(dbglvl, "plugin list is NULL\n");
553 int num = plugin_list->size();
556 Dmsg0(dbglvl, "No plugins loaded\n");
560 jcr->plugin_ctx_list = (bpContext *)malloc(sizeof(bpContext) * num);
562 bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list;
563 Dmsg2(dbglvl, "Instantiate plugin_ctx=%p JobId=%d\n", plugin_ctx_list, jcr->JobId);
564 foreach_alist(plugin, plugin_list) {
565 /* Start a new instance of each plugin */
566 bacula_ctx *b_ctx = (bacula_ctx *)malloc(sizeof(bacula_ctx));
567 memset(b_ctx, 0, sizeof(bacula_ctx));
569 plugin_ctx_list[i].bContext = (void *)b_ctx; /* Bacula private context */
570 plugin_ctx_list[i].pContext = NULL;
571 if (plug_func(plugin)->newPlugin(&plugin_ctx_list[i++]) != bRC_OK) {
572 b_ctx->disabled = true;
578 * Free the plugin instances for this Job
580 void free_plugins(JCR *jcr)
585 if (!plugin_list || !jcr->plugin_ctx_list) {
586 return; /* no plugins, nothing to do */
589 bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list;
590 Dmsg2(dbglvl, "Free instance plugin_ctx=%p JobId=%d\n", plugin_ctx_list, jcr->JobId);
591 foreach_alist(plugin, plugin_list) {
592 /* Free the plugin instance */
593 plug_func(plugin)->freePlugin(&plugin_ctx_list[i]);
594 free(plugin_ctx_list[i++].bContext); /* free Bacula private context */
596 free(plugin_ctx_list);
597 jcr->plugin_ctx_list = NULL;
600 static int my_plugin_bopen(BFILE *bfd, const char *fname, int flags, mode_t mode)
603 Plugin *plugin = (Plugin *)jcr->plugin;
606 Dmsg1(dbglvl, "plugin_bopen flags=%x\n", flags);
607 io.pkt_size = sizeof(io);
608 io.pkt_end = sizeof(io);
617 plug_func(plugin)->pluginIO(jcr->plugin_ctx, &io);
618 bfd->berrno = io.io_errno;
620 errno = b_errno_win32;
623 bfd->lerror = io.lerror;
625 Dmsg1(50, "Return from plugin open status=%d\n", io.status);
629 static int my_plugin_bclose(BFILE *bfd)
632 Plugin *plugin = (Plugin *)jcr->plugin;
634 Dmsg0(dbglvl, "===== plugin_bclose\n");
635 io.pkt_size = sizeof(io);
636 io.pkt_end = sizeof(io);
642 plug_func(plugin)->pluginIO(jcr->plugin_ctx, &io);
643 bfd->berrno = io.io_errno;
645 errno = b_errno_win32;
648 bfd->lerror = io.lerror;
650 Dmsg1(dbglvl, "plugin_bclose stat=%d\n", io.status);
654 static ssize_t my_plugin_bread(BFILE *bfd, void *buf, size_t count)
657 Plugin *plugin = (Plugin *)jcr->plugin;
659 Dmsg0(dbglvl, "plugin_bread\n");
660 io.pkt_size = sizeof(io);
661 io.pkt_end = sizeof(io);
664 io.buf = (char *)buf;
667 plug_func(plugin)->pluginIO(jcr->plugin_ctx, &io);
668 bfd->berrno = io.io_errno;
670 errno = b_errno_win32;
673 bfd->lerror = io.lerror;
675 return (ssize_t)io.status;
678 static ssize_t my_plugin_bwrite(BFILE *bfd, void *buf, size_t count)
681 Plugin *plugin = (Plugin *)jcr->plugin;
683 Dmsg0(dbglvl, "plugin_bwrite\n");
684 io.pkt_size = sizeof(io);
685 io.pkt_end = sizeof(io);
688 io.buf = (char *)buf;
691 plug_func(plugin)->pluginIO(jcr->plugin_ctx, &io);
692 bfd->berrno = io.io_errno;
694 errno = b_errno_win32;
697 bfd->lerror = io.lerror;
699 return (ssize_t)io.status;
702 static boffset_t my_plugin_blseek(BFILE *bfd, boffset_t offset, int whence)
705 Plugin *plugin = (Plugin *)jcr->plugin;
707 Dmsg0(dbglvl, "plugin_bseek\n");
708 io.pkt_size = sizeof(io);
709 io.pkt_end = sizeof(io);
715 plug_func(plugin)->pluginIO(jcr->plugin_ctx, &io);
716 bfd->berrno = io.io_errno;
718 errno = b_errno_win32;
721 bfd->lerror = io.lerror;
723 return (boffset_t)io.offset;
726 /* ==============================================================
728 * Callbacks from the plugin
730 * ==============================================================
732 static bRC baculaGetValue(bpContext *ctx, bVariable var, void *value)
735 jcr = ((bacula_ctx *)ctx->bContext)->jcr;
736 // Dmsg1(dbglvl, "bacula: baculaGetValue var=%d\n", var);
737 if (!value || !ctx) {
740 jcr = ((bacula_ctx *)ctx->bContext)->jcr;
744 // Dmsg1(dbglvl, "Bacula: jcr=%p\n", jcr);
747 *((int *)value) = jcr->JobId;
748 Dmsg1(dbglvl, "Bacula: return bVarJobId=%d\n", jcr->JobId);
751 *((char **)value) = my_name;
752 Dmsg1(dbglvl, "Bacula: return my_name=%s\n", my_name);
765 static bRC baculaSetValue(bpContext *ctx, bVariable var, void *value)
767 if (!value || !ctx) {
770 Dmsg1(dbglvl, "bacula: baculaSetValue var=%d\n", var);
774 static bRC baculaRegisterEvents(bpContext *ctx, ...)
784 while ((event = va_arg(args, uint32_t))) {
785 Dmsg1(dbglvl, "Plugin wants event=%u\n", event);
791 static bRC baculaJobMsg(bpContext *ctx, const char *file, int line,
792 int type, utime_t mtime, const char *fmt, ...)
799 jcr = ((bacula_ctx *)ctx->bContext)->jcr;
804 va_start(arg_ptr, fmt);
805 bvsnprintf(buf, sizeof(buf), fmt, arg_ptr);
807 Jmsg(jcr, type, mtime, "%s", buf);
811 static bRC baculaDebugMsg(bpContext *ctx, const char *file, int line,
812 int level, const char *fmt, ...)
817 va_start(arg_ptr, fmt);
818 bvsnprintf(buf, sizeof(buf), fmt, arg_ptr);
820 d_msg(file, line, level, "%s", buf);
824 static void *baculaMalloc(bpContext *ctx, const char *file, int line,
828 return sm_malloc(file, line, size);
834 static void baculaFree(bpContext *ctx, const char *file, int line, void *mem)
837 sm_free(file, line, mem);
847 int (*plugin_bopen)(JCR *jcr, const char *fname, int flags, mode_t mode) = NULL;
848 int (*plugin_bclose)(JCR *jcr) = NULL;
849 ssize_t (*plugin_bread)(JCR *jcr, void *buf, size_t count) = NULL;
850 ssize_t (*plugin_bwrite)(JCR *jcr, void *buf, size_t count) = NULL;
851 boffset_t (*plugin_blseek)(JCR *jcr, boffset_t offset, int whence) = NULL;
853 int save_file(JCR *jcr, FF_PKT *ff_pkt, bool top_level)
858 bool set_cmd_plugin(BFILE *bfd, JCR *jcr)
863 int main(int argc, char *argv[])
865 char plugin_dir[1000];
870 strcpy(my_name, "test-fd");
872 getcwd(plugin_dir, sizeof(plugin_dir)-1);
873 load_fd_plugins(plugin_dir);
881 generate_plugin_event(jcr1, bEventJobStart, (void *)"Start Job 1");
882 generate_plugin_event(jcr1, bEventJobEnd);
883 generate_plugin_event(jcr2, bEventJobStart, (void *)"Start Job 2");
885 generate_plugin_event(jcr2, bEventJobEnd);
890 Dmsg0(dbglvl, "bacula: OK ...\n");
896 #endif /* TEST_PROGRAM */