]> git.ipfire.org Git - thirdparty/bacula.git/commitdiff
Plugins: Add a common Bacula FD Plugin library.
authorRadosław Korzeniewski <radoslaw@korzeniewski.net>
Mon, 23 Nov 2020 12:02:23 +0000 (13:02 +0100)
committerEric Bollengier <eric@baculasystems.com>
Thu, 24 Mar 2022 08:02:59 +0000 (09:02 +0100)
bacula/src/plugins/fd/pluginlib/Makefile.in [new file with mode: 0644]
bacula/src/plugins/fd/pluginlib/pluginlib.cpp [new file with mode: 0644]
bacula/src/plugins/fd/pluginlib/pluginlib.h [new file with mode: 0644]
bacula/src/plugins/fd/pluginlib/pluginlib_test.cpp [new file with mode: 0644]
bacula/src/plugins/fd/pluginlib/ptcomm.cpp [new file with mode: 0644]
bacula/src/plugins/fd/pluginlib/ptcomm.h [new file with mode: 0644]

diff --git a/bacula/src/plugins/fd/pluginlib/Makefile.in b/bacula/src/plugins/fd/pluginlib/Makefile.in
new file mode 100644 (file)
index 0000000..c061160
--- /dev/null
@@ -0,0 +1,101 @@
+#
+# Makefile for building FD plugins PluginLibrary for Bacula
+#
+# Copyright (C) 2000-2020 Kern Sibbald
+# License: BSD 2-Clause; see file LICENSE-FOSS
+
+#  Author: Radoslaw Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o.
+
+@MCOMMON@
+
+# No optimization for now for easy debugging
+
+SRCDIR = ../../..
+FDDIR = $(SRCDIR)/filed
+LIBDIR = $(SRCDIR)/lib
+FINDLIBDIR = $(SRCDIR)/findlib
+
+topdir = @BUILD_DIR@
+thisdir = src/plugins/fd/pluginlib
+
+UNITTESTSOBJ = $(LIBDIR)/unittests.lo
+
+PLUGINLIBSSRC = pluginlib.cpp pluginlib.h
+PLUGINLIBSOBJ = $(filter %.lo,$(PLUGINLIBSSRC:.cpp=.lo))
+ISO8601SRC = iso8601.cpp iso8601.h
+ISO8601OBJ = $(filter %.lo,$(ISO8601SRC:.cpp=.lo))
+EXECPROGSRC = execprog.cpp execprog.h
+EXECPROGOBJ = $(filter %.lo,$(EXECPROGSRC:.cpp=.lo))
+COMMCTXSRC = commctx.cpp commctx.h
+COMMCTXOBJ = $(filter %.lo,$(COMMCTXSRC:.cpp=.lo))
+
+PLUGINLIBSTEST = pluginlib_test.cpp $(PLUGINLIBSSRC) $(UNITTESTSOBJ)
+PLUGINLIBSTESTOBJ = $(filter %.lo,$(PLUGINLIBSTEST:.cpp=.lo))
+ISO8601TEST = iso8601_test.cpp $(ISO8601SRC) $(UNITTESTSOBJ)
+ISO8601TESTOBJ = $(filter %.lo,$(ISO8601TEST:.cpp=.lo))
+
+COMMONPLUGINOBJ = $(PLUGINLIBSOBJ) $(ISO8601OBJ) $(EXECPROGOBJ)
+COMMONPLUGINTESTS = pluginlib_test iso8601_test
+
+.SUFFIXES:    .c .lo
+
+LIBBAC = -lbac -L$(LIBDIR)/.libs
+
+.c.lo:
+       @echo "Compiling $< ..."
+       $(NO_ECHO)$(LIBTOOL_COMPILE) $(CXX) $(DEFS) $(DEBUG) $(CPPFLAGS) $(CFLAGS) -I$(SRCDIR) -I$(FDDIR) -I$(LIBDIR) -I$(FINDLIBDIR) -I. -c $<
+
+.cpp.lo:
+       @echo "Compiling c++ $< ..."
+       $(NO_ECHO)$(LIBTOOL_COMPILE) $(CXX) $(DEFS) $(DEBUG) $(CPPFLAGS) $(CFLAGS) -I$(SRCDIR) -I$(FDDIR) -I$(LIBDIR) -I$(FINDLIBDIR) -I. -c $<
+
+all: $(COMMONPLUGINOBJ) $(COMMONPLUGINTESTS)
+
+$(LIBDIR)/unittests.lo:
+       $(MAKE) -C $(LIBDIR) unittests.lo
+
+pluginlib_test: Makefile $(PLUGINLIBSTESTOBJ) $(PLUGINLIBSSRC)
+       @echo "Building $@ ..."
+       $(NO_ECHO)$(LIBTOOL_LINK) --silent $(CXX) $(LDFLAGS) $(LIBCURL) $(LIBBAC) $(PLUGINLIBSTESTOBJ) -o $@
+
+iso8601_test: Makefile $(ISO8601TESTOBJ) $(ISO8601SRC)
+       @echo "Building $@ ..."
+       $(NO_ECHO)$(LIBTOOL_LINK) --silent $(CXX) $(LDFLAGS) $(LIBCURL) $(LIBBAC) $(PLUGINLIBSTESTOBJ) -o $@
+
+install: all
+       $(MKDIR) $(DESTDIR)$(plugindir)
+       $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) bpipe-fd.la $(DESTDIR)$(plugindir)
+       $(RMF) $(DESTDIR)$(plugindir)/bpipe-fd.la
+
+install-test-plugin: all
+       $(MKDIR) $(DESTDIR)$(plugindir)
+       $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) test-plugin-fd.la $(DESTDIR)$(plugindir)
+       $(RMF) $(DESTDIR)$(plugindir)/test-plugin-fd.la
+       $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) test-deltaseq-fd.la $(DESTDIR)$(plugindir)
+       $(RMF) $(DESTDIR)$(plugindir)/test-deltaseq-fd.la
+       $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) test-handlexacl-plugin-fd.la $(DESTDIR)$(plugindir)
+       $(RMF) $(DESTDIR)$(plugindir)/test-handlexacl-plugin-fd.la
+
+Makefile: Makefile.in $(topdir)/config.status
+       cd $(topdir) \
+         && CONFIG_FILES=$(thisdir)/$@ CONFIG_HEADERS= $(SHELL) ./config.status
+
+libtool-clean:
+       @find . -name '*.lo' -print | xargs $(LIBTOOL_CLEAN) $(RMF)
+       @$(RMF) *.la
+       @$(RMF) -r .libs _libs
+
+clean: libtool-clean
+       @rm -f main *.so *.o 1 2 3
+       @rm -f $(COMMONPLUGINTESTS)
+
+distclean: clean
+       @rm -f Makefile *.la *.lo
+       @rm -rf .libs
+
+libtool-uninstall:
+       $(LIBTOOL_UNINSTALL) $(RMF) $(DESTDIR)$(plugindir)/bpipe-fd.so
+
+uninstall: @LIBTOOL_UNINSTALL_TARGET@
+
+depend:
diff --git a/bacula/src/plugins/fd/pluginlib/pluginlib.cpp b/bacula/src/plugins/fd/pluginlib/pluginlib.cpp
new file mode 100644 (file)
index 0000000..0982b19
--- /dev/null
@@ -0,0 +1,499 @@
+/*
+   Bacula® - The Network Backup Solution
+
+   Copyright (C) 2007-2017 Bacula Systems SA
+   All rights reserved.
+
+   The main author of Bacula is Kern Sibbald, with contributions from many
+   others, a complete list can be found in the file AUTHORS.
+
+   Licensees holding a valid Bacula Systems SA license may use this file
+   and others of this release in accordance with the proprietary license
+   agreement provided in the LICENSE file.  Redistribution of any part of
+   this release is not permitted.
+
+   Bacula® is a registered trademark of Kern Sibbald.
+*/
+/*
+ *
+ * All rights reserved. IP transferred to Bacula Systems according to agreement.
+ *
+ * Common definitions and utility functions for Inteos plugins.
+ * Functions defines a common framework used in our utilities and plugins.
+ * Author: Radosław Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o.
+ */
+
+#include "pluginlib.h"
+
+
+/* Events that are passed to plugin
+typedef enum {
+  bEventJobStart                        = 1,
+  bEventJobEnd                          = 2,
+  bEventStartBackupJob                  = 3,
+  bEventEndBackupJob                    = 4,
+  bEventStartRestoreJob                 = 5,
+  bEventEndRestoreJob                   = 6,
+  bEventStartVerifyJob                  = 7,
+  bEventEndVerifyJob                    = 8,
+  bEventBackupCommand                   = 9,
+  bEventRestoreCommand                  = 10,
+  bEventEstimateCommand                 = 11,
+  bEventLevel                           = 12,
+  bEventSince                           = 13,
+  bEventCancelCommand                   = 14,
+  bEventVssBackupAddComponents          = 15,
+  bEventVssRestoreLoadComponentMetadata = 16,
+  bEventVssRestoreSetComponentsSelected = 17,
+  bEventRestoreObject                   = 18,
+  bEventEndFileSet                      = 19,
+  bEventPluginCommand                   = 20,
+  bEventVssBeforeCloseRestore           = 21,
+  bEventVssPrepareSnapshot              = 22,
+  bEventOptionPlugin                    = 23,
+  bEventHandleBackupFile                = 24,
+  bEventComponentInfo                   = 25
+} bEventType;
+*/
+
+const char *eventtype2str(bEvent *event){
+   switch (event->eventType){
+      case bEventJobStart:
+         return "bEventJobStart";
+      case bEventJobEnd:
+         return "bEventJobEnd";
+      case bEventStartBackupJob:
+         return "bEventStartBackupJob";
+      case bEventEndBackupJob:
+         return "bEventEndBackupJob";
+      case bEventStartRestoreJob:
+         return "bEventStartRestoreJob";
+      case bEventEndRestoreJob:
+         return "bEventEndRestoreJob";
+      case bEventStartVerifyJob:
+         return "bEventStartVerifyJob";
+      case bEventEndVerifyJob:
+         return "bEventEndVerifyJob";
+      case bEventBackupCommand:
+         return "bEventBackupCommand";
+      case bEventRestoreCommand:
+         return "bEventRestoreCommand";
+      case bEventEstimateCommand:
+         return "bEventEstimateCommand";
+      case bEventLevel:
+         return "bEventLevel";
+      case bEventSince:
+         return "bEventSince";
+      case bEventCancelCommand:
+         return "bEventCancelCommand";
+      case bEventVssBackupAddComponents:
+         return "bEventVssBackupAddComponents";
+      case bEventVssRestoreLoadComponentMetadata:
+         return "bEventVssRestoreLoadComponentMetadata";
+      case bEventVssRestoreSetComponentsSelected:
+         return "bEventVssRestoreSetComponentsSelected";
+      case bEventRestoreObject:
+         return "bEventRestoreObject";
+      case bEventEndFileSet:
+         return "bEventEndFileSet";
+      case bEventPluginCommand:
+         return "bEventPluginCommand";
+      case bEventVssBeforeCloseRestore:
+         return "bEventVssBeforeCloseRestore";
+      case bEventVssPrepareSnapshot:
+         return "bEventVssPrepareSnapshot";
+      case bEventOptionPlugin:
+         return "bEventOptionPlugin";
+      case bEventHandleBackupFile:
+         return "bEventHandleBackupFile";
+      case bEventComponentInfo:
+         return "bEventComponentInfo";
+      default:
+         return "Unknown";
+   }
+}
+
+
+/*
+ * Return the real size of the disk based on the size suffix.
+ *
+ * in:
+ *    disksize - the numeric value of the disk size to compute
+ *    suff - the suffix for a disksize value
+ * out:
+ *    uint64_t - the size of the disk computed with suffix
+ */
+uint64_t pluglib_size_suffix(int disksize, char suff)
+{
+   uint64_t size;
+
+   switch (suff){
+      case 'G':
+         size = (uint64_t)disksize * 1024 * 1048576;
+         break;
+      case 'M':
+         size = (uint64_t)disksize * 1048576;
+         break;
+      case 'T':
+         size = (uint64_t)disksize * 1048576 * 1048576;
+         break;
+      case 'K':
+      case 'k':
+         size = (uint64_t)disksize * 1024;
+         break;
+      default:
+         size = disksize;
+   }
+   return size;
+}
+
+/*
+ * Return the real size of the disk based on the size suffix.
+ *    This version uses a floating point numbers (double) for computation.
+ *
+ * in:
+ *    disksize - the numeric value of the disk size to compute
+ *    suff - the suffix for a disksize value
+ * out:
+ *    uint64_t - the size of the disk computed with suffix
+ */
+uint64_t pluglib_size_suffix(double disksize, char suff)
+{
+   uint64_t size;
+
+   switch (suff){
+      case 'G':
+         size = disksize * 1024.0 * 1048576.0;
+         break;
+      case 'M':
+         size = disksize * 1048576.0;
+         break;
+      case 'T':
+         size = disksize * 1048576.0 * 1048576.0;
+         break;
+      case 'K':
+      case 'k':
+         size = disksize * 1024.0;
+         break;
+      default:
+         size = disksize;
+   }
+   return size;
+}
+
+/*
+ * Creates a path hierarchy on local FS.
+ *  It is used for local restore mode to create a required directory.
+ *  The functionality is similar to 'mkdir -p'.
+ *
+ * TODO: make a support for relative path
+ * TODO: check if we can use findlib/makepath implementation instead
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    path - a full path to create, does not check if the path is relative,
+ *           could fail in this case
+ * out:
+ *    bRC_OK - path creation was successful
+ *    bRC_Error - on any error
+ */
+bRC pluglib_mkpath(bpContext* ctx, char* path, bool isfatal)
+{
+#ifdef PLUGINPREFIX
+#define _OLDPREFIX   PLUGINPREFIX
+#endif
+#define PLUGINPREFIX    "pluglibmkpath:"
+   struct stat statp;
+   POOL_MEM dir(PM_FNAME);
+   char *p, *q;
+
+   if (!path){
+      return bRC_Error;
+   }
+   if (stat(path, &statp) == 0){
+      if (S_ISDIR(statp.st_mode)){
+         return bRC_OK;
+      } else {
+         DMSG(ctx, DERROR, "Path %s is not directory\n", path);
+         JMSG(ctx, isfatal ? M_FATAL : M_ERROR, "Path %s is not directory\n", path);
+         return bRC_Error;
+      }
+   }
+   DMSG(ctx, DDEBUG, "mkpath verify dir: %s\n", path);
+   pm_strcpy(dir, path);
+   p = dir.addr() + 1;
+   while (*p && (q = strchr(p, (int)PathSeparator)) != NULL){
+      *q = 0;
+      DMSG(ctx, DDEBUG, "mkpath scanning(1): %s\n", dir.c_str());
+      if (stat(dir.c_str(), &statp) == 0){
+         *q = PathSeparator;
+         p = q + 1;
+         continue;
+      }
+      DMSG0(ctx, DDEBUG, "mkpath will create dir(1).\n");
+      if (mkdir(dir.c_str(), 0750) < 0){
+         /* error */
+         berrno be;
+         DMSG2(ctx, DERROR, "Cannot create directory %s Err=%s\n", dir.c_str(), be.bstrerror());
+         JMSG2(ctx, isfatal ? M_FATAL : M_ERROR, "Cannot create directory %s Err=%s\n", dir.c_str(), be.bstrerror());
+         return bRC_Error;
+      }
+      *q = PathSeparator;
+      p = q + 1;
+   }
+   DMSG0(ctx, DDEBUG, "mkpath will create dir(2).\n");
+   if (mkdir(path, 0750) < 0){
+      /* error */
+      berrno be;
+      DMSG2(ctx, DERROR, "Cannot create directory %s Err=%s\n", path, be.bstrerror());
+      JMSG2(ctx, isfatal ? M_FATAL : M_ERROR, "Cannot create directory %s Err=%s\n", path, be.bstrerror());
+      return bRC_Error;
+   }
+   DMSG0(ctx, DDEBUG, "mkpath finish.\n");
+#ifdef _OLDPREFIX
+#define PLUGINPREFIX    _OLDPREFIX
+#undef _OLDPREFIX
+#else
+#undef PLUGINPREFIX
+#endif
+   return bRC_OK;
+}
+
+/**
+ * @brief
+ *
+ * @param str
+ * @param sep
+ * @return alist*
+ */
+alist * plugutil_str_split_to_alist(const char * str, const char sep)
+{
+   POOL_MEM buf(PM_NAME);
+   const char * p;
+   const char * q;
+   const char * s;
+   alist * list;
+
+   if (str == NULL || strlen(str) == 0){
+      return NULL;
+   }
+
+   list = New(alist(5, true));
+   p = str;
+
+   do {
+      // search for separator char - sep
+      q = strchr(p, sep);
+      if (q == NULL){
+         // copy whole string from p to buf
+         pm_strcpy(buf, p);
+      } else {
+         // copy string from p up to q
+         pm_memcpy(buf, p, q - p + 1);
+         buf.c_str()[q - p] = '\0';
+         p = q + 1;     // next element
+      }
+      // in buf we have splitted string part
+      s = bstrdup(buf.c_str());
+      list->append((void*)s);
+   } while (q != NULL);
+
+   return list;
+}
+
+/*
+ * Render a xe tool parameter for string value.
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    param - a pointer to the param variable where we will render a parameter
+ *    pname - a name of the parameter to compare
+ *    fmt - a low-level parameter name
+ *    name - a name of the parameter from parameter list
+ *    value - a value to render
+ * out:
+ *    True if parameter was rendered
+ *    False if it was not the parameter required
+ */
+bool render_param(POOLMEM **param, const char *pname, const char *fmt, const char *name, char *value)
+{
+   if (bstrcasecmp(name, pname)){
+      if (!*param){
+         *param = get_pool_memory(PM_NAME);
+         Mmsg(*param, " -%s '%s' ", fmt, value);
+         DMsg1(DDEBUG, "render param:%s\n", *param);
+      }
+      return true;
+   }
+   return false;
+}
+
+/*
+ * Render a xe tool parameter for integer value.
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    param - a pointer to the param variable where we will render a parameter
+ *    pname - a name of the parameter to compare
+ *    fmt - a low-level parameter name
+ *    name - a name of the parameter from parameter list
+ *    value - a value to render
+ * out:
+ *    True if parameter was rendered
+ *    False if it was not the parameter required
+ */
+bool render_param(POOLMEM **param, const char *pname, const char *fmt, const char *name, int value)
+{
+   if (bstrcasecmp(name, pname)){
+      if (!*param){
+         *param = get_pool_memory(PM_NAME);
+         Mmsg(*param, " -%s %d ", value);
+         DMsg1(DDEBUG, "render param:%s\n", *param);
+      }
+      return true;
+   }
+   return false;
+}
+
+/**
+ * @brief
+ *
+ * @param param
+ * @param pname
+ * @param name
+ * @param value
+ * @return true
+ * @return false
+ */
+bool parse_param(POOL_MEM &param, const char *pname, const char *name, char *value)
+{
+   if (bstrcasecmp(name, pname)){
+      pm_strcpy(param, value);
+      DMsg1(DDEBUG, "render param:%s\n", param.c_str());
+      return true;
+   }
+   return false;
+};
+
+/*
+ * Setup XECOMMCTX parameter for boolean value.
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    param - a pointer to the param variable where we will render a parameter
+ *    pname - a name of the parameter to compare
+ *    name - a name of the parameter from parameter list
+ *    value - a value to render
+ * out:
+ *    True if parameter was rendered
+ *    False if it was not the parameter required
+ */
+bool render_param(bool &param, const char *pname, const char *name, bool value)
+{
+   if (bstrcasecmp(name, pname)){
+      if (param){
+         param = value;
+         DMsg2(DDEBUG, "render param: %s=%s\n", pname, param ? "True" : "False");
+      }
+      return true;
+   }
+   return false;
+}
+
+/*
+ * Setup XECOMMCTX parameter for boolean from string value.
+ *  The parameter value will be false if value start with '0' character and
+ *  will be true in any other case. So, when a plugin will have a following:
+ *    param
+ *    param=xxx
+ *    param=1
+ *  then a param will be set to true.
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    param - a pointer to the param variable where we will render a parameter
+ *    pname - a name of the parameter to compare
+ *    name - a name of the parameter from parameter list
+ *    value - a value to render
+ * out:
+ *    True if parameter was rendered
+ *    False if it was not the parameter required
+ */
+bool parse_param(bool &param, const char *pname, const char *name, char *value)
+{
+   if (bstrcasecmp(name, pname)){
+      if (value && *value == '0'){
+         param = false;
+      } else {
+         param = true;
+      }
+      DMsg2(DINFO, "%s parameter: %s\n", name, param ? "True" : "False");
+      return true;
+   }
+   return false;
+}
+
+/*
+ * Setup Plugin parameter for integer from string value.
+ *
+ * in:
+ *    param - a pointer to the param variable where we will render a parameter
+ *    pname - a name of the parameter to compare
+ *    name - a name of the parameter from parameter list
+ *    value - a value to render
+ * out:
+ *    True if parameter was parsed
+ *    False if it was not the parameter required
+ */
+bool parse_param(int &param, const char *pname, const char *name, char *value, bool * err)
+{
+   // clear error flag when requested
+   if (err != NULL) *err = false;
+
+   if (value && bstrcasecmp(name, pname)){
+      /* convert str to integer */
+      param = atoi(value);
+      if (param == 0){
+         /* error in conversion */
+         DMsg2(DERROR, "Invalid %s parameter: %s\n", name, value);
+         // setup error flag
+         if (err != NULL) *err = true;
+         return false;
+      }
+      DMsg2(DINFO, "%s parameter: %d\n", name, param);
+
+      return true;
+   }
+   return false;
+}
+
+/*
+ * Render and add a parameter for string value to alist.
+ *  When alist is NULL (uninitialized) then it creates a new list to use.
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    list - pointer to alist class to use
+ *    pname - a name of the parameter to compare
+ *    name - a name of the parameter from parameter list
+ *    value - a value to render
+ * out:
+ *    True if parameter was rendered
+ *    False if it was not the parameter required
+ */
+bool add_param_str(alist **list, const char *pname, const char *name, char *value)
+{
+   POOLMEM *param;
+
+   if (bstrcasecmp(name, pname)){
+      if (!*list){
+         *list = New(alist(8, not_owned_by_alist));
+      }
+      param = get_pool_memory(PM_NAME);
+      Mmsg(param, "%s", value);
+      (*list)->append(param);
+      DMsg2(DDEBUG, "add param: %s=%s\n", name, value);
+      return true;
+   }
+   return false;
+}
diff --git a/bacula/src/plugins/fd/pluginlib/pluginlib.h b/bacula/src/plugins/fd/pluginlib/pluginlib.h
new file mode 100644 (file)
index 0000000..b475374
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+   Bacula® - The Network Backup Solution
+
+   Copyright (C) 2007-2017 Bacula Systems SA
+   All rights reserved.
+
+   The main author of Bacula is Kern Sibbald, with contributions from many
+   others, a complete list can be found in the file AUTHORS.
+
+   Licensees holding a valid Bacula Systems SA license may use this file
+   and others of this release in accordance with the proprietary license
+   agreement provided in the LICENSE file.  Redistribution of any part of
+   this release is not permitted.
+
+   Bacula® is a registered trademark of Kern Sibbald.
+*/
+/*
+ *
+ * All rights reserved. IP transferred to Bacula Systems according to agreement.
+ *
+ * Common definitions and utility functions for Inteos plugins.
+ * Functions defines a common framework used in our utilities and plugins.
+ * Author: Radosław Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o.
+ */
+
+#ifndef _PLUGINLIB_H_
+#define _PLUGINLIB_H_
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <ctype.h>
+
+#include "bacula.h"
+#include "fd_plugins.h"
+
+/* Pointers to Bacula functions used in plugins */
+extern bFuncs *bfuncs;
+extern bInfo *binfo;
+
+/* module definition */
+#ifndef PLUGMODULE
+#define PLUGMODULE   "PluginLib::"
+#endif
+
+// #ifndef PLUGINPREFIX
+// #define PLUGINPREFIX    PLUGMODULE
+// #endif
+
+/* size of different string or query buffers */
+#define BUFLEN       4096
+#define BIGBUFLEN    65536
+
+/* debug and messages functions */
+#define JMSG0(ctx,type,msg) \
+   if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg );
+#define JMSG1 JMSG
+#define JMSG(ctx,type,msg,var) \
+   if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var );
+#define JMSG2(ctx,type,msg,var1,var2) \
+   if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var1, var2 );
+#define JMSG3(ctx,type,msg,var1,var2,var3) \
+   if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var1, var2, var3 );
+#define JMSG4(ctx,type,msg,var1,var2,var3,var4) \
+   if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var1, var2, var3, var4 );
+
+#define DMSG0(ctx,level,msg) \
+   if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg );
+#define DMSG1 DMSG
+#define DMSG(ctx,level,msg,var) \
+   if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var );
+#define DMSG2(ctx,level,msg,var1,var2) \
+   if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2 );
+#define DMSG3(ctx,level,msg,var1,var2,var3) \
+   if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2, var3 );
+#define DMSG4(ctx,level,msg,var1,var2,var3,var4) \
+   if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2, var3, var4 );
+#define DMSG6(ctx,level,msg,var1,var2,var3,var4,var5,var6) \
+   if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2, var3, var4, var5, var6 );
+
+/* fixed debug level definitions */
+#define D1  1                    /* debug for every error */
+#define DERROR D1
+#define D2  10                   /* debug only important stuff */
+#define DINFO  D2
+#define D3  200                  /* debug for information only */
+#define DDEBUG D3
+#define D4  800                  /* debug for detailed information only */
+#define DVDEBUG D4
+
+#define getBaculaVar(bvar,val)  bfuncs->getBaculaValue(ctx, bvar, val);
+
+/* used for sanity check in plugin functions */
+#define ASSERT_CTX \
+   if (!ctx || !ctx->pContext || !bfuncs) \
+   { \
+      return bRC_Error; \
+   }
+
+/* defines for handleEvent */
+#define DMSG_EVENT_STR(event,value)       DMSG2(ctx, DINFO, "%s value=%s\n", eventtype2str(event), NPRT((char *)value));
+#define DMSG_EVENT_CHAR(event,value)      DMSG2(ctx, DINFO, "%s value='%c'\n", eventtype2str(event), (char)value);
+#define DMSG_EVENT_LONG(event,value)      DMSG2(ctx, DINFO, "%s value=%ld\n", eventtype2str(event), (intptr_t)value);
+#define DMSG_EVENT_PTR(event,value)       DMSG2(ctx, DINFO, "%s value=%p\n", eventtype2str(event), value);
+
+/* pure debug macros */
+#define DMsg0(level,msg)                  Dmsg1(level,PLUGMODULE "%s: " msg,__func__)
+#define DMsg1(level,msg,a1)               Dmsg2(level,PLUGMODULE "%s: " msg,__func__,a1)
+#define DMsg2(level,msg,a1,a2)            Dmsg3(level,PLUGMODULE "%s: " msg,__func__,a1,a2)
+#define DMsg3(level,msg,a1,a2,a3)         Dmsg4(level,PLUGMODULE "%s: " msg,__func__,a1,a2,a3)
+#define DMsg4(level,msg,a1,a2,a3,a4)      Dmsg5(level,PLUGMODULE "%s: " msg,__func__,a1,a2,a3,a4)
+
+#define BOOLSTR(b)                        (b?"True":"False")
+
+/*
+ * Common structure for key/pair values
+ */
+class key_pair : public SMARTALLOC
+{
+public:
+   POOL_MEM key;
+   POOL_MEM value;
+
+   key_pair() : key(PM_NAME), value(PM_MESSAGE) {};
+   key_pair(const char *k, const char *v)
+   {
+      pm_strcpy(key, k);
+      pm_strcpy(value, v);
+   };
+   ~key_pair() {};
+};
+
+const char *eventtype2str(bEvent *event);
+uint64_t pluglib_size_suffix(int disksize, char suff);
+uint64_t pluglib_size_suffix(double disksize, char suff);
+bRC pluglib_mkpath(bpContext* ctx, char* path, bool isfatal);
+
+/*
+ * Checks if plugin command points to our Plugin
+ *
+ * in:
+ *    command - the plugin command used for backup/restore
+ * out:
+ *    True - if it is our plugin command
+ *    False - the other plugin command
+ */
+inline bool isourplugincommand(const char *pluginprefix, const char *command)
+{
+   /* check if it is our Plugin command */
+   if (strncmp(pluginprefix, command, strlen(pluginprefix)) == 0){
+      /* it is not our plugin prefix */
+      return true;
+   }
+   return false;
+}
+
+alist * plugutil_str_split_to_alist(const char * str, const char sep = '.');
+
+/* plugin parameters manipulation */
+bool render_param(POOLMEM **param, const char *pname, const char *fmt, const char *name, char *value);
+bool render_param(POOLMEM **param, const char *pname, const char *fmt, const char *name, int value);
+bool render_param(bool &param, const char *pname, const char *name, bool value);
+bool parse_param(bool &param, const char *pname, const char *name, char *value);
+bool parse_param(int &param, const char *pname, const char *name, char *value, bool *err = NULL);
+bool parse_param(POOL_MEM &param, const char *pname, const char *name, char *value);
+bool add_param_str(alist **list, const char *pname, const char *name, char *value);
+
+#endif   /* _PLUGINLIB_H_ */
\ No newline at end of file
diff --git a/bacula/src/plugins/fd/pluginlib/pluginlib_test.cpp b/bacula/src/plugins/fd/pluginlib/pluginlib_test.cpp
new file mode 100644 (file)
index 0000000..3b870aa
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+   Bacula® - The Network Backup Solution
+
+   Copyright (C) 2007-2017 Bacula Systems SA
+   All rights reserved.
+
+   The main author of Bacula is Kern Sibbald, with contributions from many
+   others, a complete list can be found in the file AUTHORS.
+
+   Licensees holding a valid Bacula Systems SA license may use this file
+   and others of this release in accordance with the proprietary license
+   agreement provided in the LICENSE file.  Redistribution of any part of
+   this release is not permitted.
+
+   Bacula® is a registered trademark of Kern Sibbald.
+*/
+/*
+ *
+ * All rights reserved. IP transferred to Bacula Systems according to agreement.
+ *
+ * Common definitions and utility functions for Inteos plugins.
+ * Functions defines a common framework used in our utilities and plugins.
+ * Author: Radosław Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o.
+ */
+
+#include "pluginlib.h"
+#include "unittests.h"
+
+bFuncs *bfuncs;
+bInfo *binfo;
+
+int main()
+{
+   Unittests pluglib_test("pluglib_test");
+   alist * list;
+   char * s;
+
+   // Pmsg0(0, "Initialize tests ...\n");
+
+   list = plugutil_str_split_to_alist("123456789");
+   ok(list != NULL, "default split");
+   ok(list->size() == 1, "expect single strings");
+   foreach_alist(s, list){
+      ok(strlen(s) == 9, "check element length");
+   }
+   delete list;
+
+   list = plugutil_str_split_to_alist("123.456");
+   ok(list != NULL, "split: 123.456");
+   ok(list->size() == 2, "expect two strings");
+   foreach_alist(s, list){
+      ok(strlen(s) == 3, "check element length");
+   }
+   delete list;
+
+   list = plugutil_str_split_to_alist("12345.56789.abcde");
+   ok(list != NULL, "split: 12345.56789.abcde");
+   ok(list->size() == 3, "expect three strings");
+   foreach_alist(s, list){
+      ok(strlen(s) == 5, "check element length");
+   }
+   delete list;
+
+   list = plugutil_str_split_to_alist("1.bacula..Eric.Kern");
+   ok(list != NULL, "split: 1.bacula..Eric.Kern");
+   ok(list->size() == 5, "expect three strings");
+   ok(strcmp((char*)list->first(), "1") == 0, "check element 1");
+   ok(strcmp((char*)list->next(), "bacula") == 0, "check element bacula");
+   ok(strlen((char*)list->next()) == 0, "check empty element");
+   ok(strcmp((char*)list->next(), "Eric") == 0, "check element Eric");
+   ok(strcmp((char*)list->next(), "Kern") == 0, "check element Kern");
+   delete list;
+
+   return report();
+}
diff --git a/bacula/src/plugins/fd/pluginlib/ptcomm.cpp b/bacula/src/plugins/fd/pluginlib/ptcomm.cpp
new file mode 100644 (file)
index 0000000..943555e
--- /dev/null
@@ -0,0 +1,834 @@
+/*
+   Bacula® - The Network Backup Solution
+
+   Copyright (C) 2007-2017 Bacula Systems SA
+   All rights reserved.
+
+   The main author of Bacula is Kern Sibbald, with contributions from many
+   others, a complete list can be found in the file AUTHORS.
+
+   Licensees holding a valid Bacula Systems SA license may use this file
+   and others of this release in accordance with the proprietary license
+   agreement provided in the LICENSE file.  Redistribution of any part of
+   this release is not permitted.
+
+   Bacula® is a registered trademark of Kern Sibbald.
+*/
+/**
+ * @file ptcomm.cpp
+ * @author Radosław Korzeniewski (radoslaw@korzeniewski.net)
+ * @brief This is a Bacula plugin library for interfacing with Metaplugin backend.
+ * @version 2.0.0
+ * @date 2020-11-20
+ *
+ * @copyright Copyright (c) 2020
+ */
+
+#include "ptcomm.h"
+#include <sys/stat.h>
+#include <signal.h>
+
+/* Plugin compile time variables required by pluglib */
+#define PLUGINPREFIX            "ptcomm:"
+
+/*
+ * libbac uses its own sscanf implementation which is not compatible with
+ * libc implementation, unfortunately.
+ * use bsscanf for Bacula sscanf flavor
+ */
+#ifdef sscanf
+#undef sscanf
+#endif
+// #define NEED_REVIEW
+
+/* from lib/scan.c */
+extern int parse_args(POOLMEM *cmd, POOLMEM **args, int *argc,
+                      char **argk, char **argv, int max_args);
+
+/*
+ * Closes external pipe if available (opened).
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ * out:
+ *    true - when closed without a problem
+ *    false - when got any error
+ */
+bool PTCOMM::close_extpipe(bpContext *ctx)
+{
+   int rc;
+
+   /* close expipe if used */
+   if (extpipe > 0){
+      rc = close(extpipe);
+      extpipe = -1;
+      if (rc != 0){
+         berrno be;
+         DMSG(ctx, DERROR, "Cannot close ExtPIPE. Err=%s\n", be.bstrerror());
+         JMSG(ctx, M_ERROR, "Cannot close ExtPIPE. Err=%s\n", be.bstrerror());
+         return false;
+      }
+   }
+   return true;
+}
+
+/*
+ * Terminate the connection represented by BPIPE object.
+ *    it shows a debug and job messages when connection close is unsuccessful
+ *    and when ctx is available only.
+ *
+ * in:
+ *    bpContext - Bacula Plugin context required for debug/job messages to show,
+ *                it could be NULL in this case no messages will be shown
+ * out:
+ *    none
+ */
+void PTCOMM::terminate(bpContext *ctx)
+{
+   if (is_closed())
+      return;
+
+   pid_t worker_pid = bpipe->worker_pid;
+   int status = close_bpipe(bpipe);
+
+   bpipe = NULL;     // indicte closed bpipe
+
+   if (status && ctx)
+   {
+      /* error during close */
+      berrno be;
+      DMSG(ctx, DERROR, "Error closing backend. Err=%s\n", be.bstrerror(status));
+      JMSG(ctx, M_ERROR, "Error closing backend. Err=%s\n", be.bstrerror(status));
+   }
+
+   if (worker_pid)
+   {
+      /* terminate the backend */
+      kill(worker_pid, SIGTERM);
+   }
+
+   if (extpipe > 0)
+      close_extpipe(ctx);
+};
+
+/**
+ * @brief Reads `nbytes` of data from backend into a buffer `buf`.
+ *
+ * This is a dedicated method for reading raw data from backend.
+ * It reads exact `nbytes` number of bytes and stores it at `buf`.
+ * It will not return until all requested data is ready or got error.
+ * You have to use it when you known exact number of bytes to read from
+ * the backend. The method handles errors and timeout reading data.
+ *
+ * @param ctx - for Bacula debug jobinfo messages
+ * @param buf - the memory buffer where we will read data
+ * @param nbytes - the exact number of bytes to read into `buf`
+ * @return true - when read was successful
+ * @return false - on any error
+ */
+bool PTCOMM::recvbackend_data(bpContext *ctx, char *buf, int32_t nbytes)
+{
+   int status;
+   int rbytes = 0;
+
+   _timeout.tv_sec = PTCOMM_DEFAULT_TIMEOUT;
+   _timeout.tv_usec = 0;
+
+   while (nbytes)
+   {
+      fd_set rfds;
+
+      FD_ZERO(&rfds);
+      FD_SET(rfd, &rfds);
+      FD_SET(efd, &rfds);
+
+      status = select(maxfd, &rfds, NULL, NULL, &_timeout);
+      if (status == 0)
+      {
+         // this means timeout waiting
+         f_error = true;
+         DMSG1(ctx, DERROR, "BPIPE read timeout=%d.\n", _timeout.tv_sec);
+         JMSG1(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE read timeout=%d.\n", _timeout.tv_sec);
+         return false;
+      }
+
+      // check if any data on error channel
+      if (FD_ISSET(efd, &rfds))
+      {
+         // do read of error channel
+         f_error = true;
+         status = read(efd, errmsg.c_str(), errmsg.size() - 1);
+         errmsg.c_str()[status] = '\0'; // terminate string
+         strip_trailing_junk(errmsg.c_str());
+         if (status < 0)
+         {
+            /* show any error during message read */
+            berrno be;
+            DMSG(ctx, DERROR, "BPIPE read error on error channel: ERR=%s\n", be.bstrerror());
+            JMSG(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE read error on error channel: ERR=%s\n", be.bstrerror());
+         } else {
+            // got data on error channel, report it
+            DMSG1(ctx, DERROR, "Backend reported error: %s\n", errmsg.c_str());
+            JMSG1(ctx, is_fatal() ? M_FATAL : M_ERROR, "Backend reported error: %s\n", errmsg.c_str());
+         }
+      }
+
+      // check if data descriptor is ready
+      if (FD_ISSET(rfd, &rfds))
+      {
+         // do read of data
+         status = read(rfd, buf + rbytes, nbytes);
+         if (status < 0)
+         {
+            /* show any error during data read */
+            berrno be;
+            f_error = true;
+            DMSG(ctx, DERROR, "BPIPE read error: ERR=%s\n", be.bstrerror());
+            JMSG(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE read error: ERR=%s\n", be.bstrerror());
+            return false;
+         }
+         if (status == 0){
+            /* the backend closed the connection without terminate signal 'T' */
+            f_error = true;
+            DMSG0(ctx, DERROR, "Backend closed the connection.\n");
+            JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "Backend closed the connection.\n");
+            return false;
+         }
+         nbytes -= status;
+         rbytes += status;
+      }
+   }
+
+   return true;
+}
+
+/**
+ * @brief
+ *
+ * @param ctx
+ * @param buf
+ * @param nbytes
+ * @return true
+ * @return false
+ */
+bool PTCOMM::sendbackend_data(bpContext *ctx, POOLMEM *buf, int32_t nbytes)
+{
+   int status;
+   int wbytes = 0;
+
+   _timeout.tv_sec = PTCOMM_DEFAULT_TIMEOUT;
+   _timeout.tv_usec = 0;
+
+   while (nbytes)
+   {
+      fd_set rfds;
+      fd_set wfds;
+
+      FD_ZERO(&rfds);
+      FD_ZERO(&wfds);
+      FD_SET(efd, &rfds);
+      FD_SET(wfd, &wfds);
+
+      status = select(maxfd, &rfds, &wfds, NULL, &_timeout);
+      if (status == 0)
+      {
+         // this means timeout waiting
+         f_error = true;
+         DMSG1(ctx, DERROR, "BPIPE write timeout=%d.\n", _timeout.tv_sec);
+         JMSG1(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE write timeout=%d.\n", _timeout.tv_sec);
+         return false;
+      }
+
+      // check if any data on error channel
+      if (FD_ISSET(efd, &rfds))
+      {
+         // do read of error channel
+         f_error = true;
+         status = read(efd, errmsg.c_str(), errmsg.size());
+         if (status < 0)
+         {
+            /* show any error during message read */
+            berrno be;
+            DMSG(ctx, DERROR, "BPIPE read error on error channel: ERR=%s\n", be.bstrerror());
+            JMSG(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE read error on error channel: ERR=%s\n", be.bstrerror());
+         } else {
+            // got data on error channel, report it
+            DMSG1(ctx, DERROR, "Backend reported error: %s\n", errmsg.c_str());
+            JMSG1(ctx, is_fatal() ? M_FATAL : M_ERROR, "Backend reported error: %s\n", errmsg.c_str());
+         }
+      }
+
+      // check if data descriptor is ready
+      if (FD_ISSET(wfd, &wfds))
+      {
+         // do write of data
+         status = write(wfd, buf + wbytes, nbytes);
+         if (status < 0)
+         {
+            /* show any error during data write */
+            berrno be;
+            f_error = true;
+            DMSG(ctx, DERROR, "BPIPE write error: ERR=%s\n", be.bstrerror());
+            JMSG(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE write error: ERR=%s\n", be.bstrerror());
+            return false;
+         }
+         nbytes -= status;
+         wbytes += status;
+      }
+   }
+
+   return true;
+}
+
+/**
+ * @brief Reads a protocol header from backend and return payload length.
+ *
+ * This method should be used at the start of every read from backend.
+ * It handles a full protocol chatting, i.e. error, warning and information
+ * messages besides EOD or termination.
+ *
+ * @param ctx - for Bacula debug jobinfo messages
+ * @param cmd - an expected command to read: `C` or `D`
+ * @return int32_t - the size of the packet payload
+ */
+int32_t PTCOMM::recvbackend_header(bpContext *ctx, char cmd)
+{
+   if (is_closed()){
+      DMSG0(ctx, DERROR, "BPIPE to backend is closed, cannot receive data.\n");
+      JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE to backend is closed, cannot receive data.\n");
+      return -1;
+   }
+
+   PTHEADER header;
+   bool workdone = false;
+
+   f_eod = f_error = f_fatal = false;
+   int32_t nbytes = sizeof(PTHEADER);
+
+   while (!workdone){
+      if (!recvbackend_data(ctx, (char*)&header, nbytes))
+      {
+         DMSG0(ctx, DERROR, "PTCOMM cannot get packet header from backend.\n");
+         JMSG0(ctx, M_FATAL, "PTCOMM cannot get packet header from backend.\n");
+         f_eod = f_error = f_fatal = true;
+         return -1;
+      }
+      DMSG(ctx, DDEBUG, "RECV: %c\n", header.status);
+
+      /* check for protocol status */
+      if (header.status == 'F'){
+         /* signal EOD */
+         f_eod = true;
+         return 0;
+      }
+
+      if (header.status == 'T'){
+         /* backend signaled a connection termination */
+         terminate(ctx);
+         return 0;
+      }
+
+      // other packet commands require data
+      header.length[6] = 0; /* end of string */
+
+      // convert packet length from ASCII to binary
+      int32_t msglen = atoi(header.length);
+
+      if (header.status == 'C' || header.status == 'D')
+      {
+         if (header.status != cmd)
+         {
+            DMSG2(ctx, DERROR, "Protocol error. Expected packet: %c got: %c\n", cmd, header.status);
+            JMSG2(ctx, M_FATAL, "Protocol error. Expected packet: %c got: %c\n", cmd, header.status);
+            return -1;
+         }
+
+         // this means no additional handling required
+         return msglen;
+      }
+
+      // need a space for nul and newline char at the end of the message
+      errmsg.check_size(msglen + 2);
+
+      // read the rest of the package
+      if (!recvbackend_data(ctx, errmsg.c_str(), msglen))
+      {
+         DMSG0(ctx, DERROR, "PTCOMM cannot get message from backend.\n");
+         JMSG0(ctx, M_FATAL, "PTCOMM cannot get message from backend.\n");
+         return -1;
+      }
+
+      // ensure error message is terminated with newline and
+      // terminated with standard c-string nul
+      errmsg.c_str()[msglen] = errmsg.c_str()[msglen - 1] != '\n' ? '\n' : '\0';
+      errmsg.c_str()[msglen + 1] = '\0';
+
+      switch (header.status)
+      {
+      /* backend signal errors */
+      case 'E':
+      case 'A':
+         /* setup error flags */
+         f_error = true;
+         f_fatal = header.status == 'A';
+
+         /* show error to Bacula */
+         DMSG(ctx, DERROR, "Backend Error: %s", errmsg.c_str());
+         JMSG(ctx, f_fatal ? M_FATAL : M_ERROR, "%s", errmsg.c_str());
+         workdone = true;
+         break;
+
+      // handle warning and info messages below
+      case 'W':
+         // handle warning message
+         DMSG(ctx, DERROR, "%s", errmsg.c_str());
+         JMSG(ctx, M_WARNING, "%s", errmsg.c_str());
+         continue;
+
+      case 'I':
+         // handle information message
+         DMSG(ctx, DINFO, "%s", errmsg.c_str());
+         JMSG(ctx, M_INFO, "%s", errmsg.c_str());
+         continue;
+
+      default:
+         DMSG1(ctx, DERROR, "Protocol error. Unknown packet: %c got: %c\n", header.status);
+         JMSG1(ctx, M_FATAL, "Protocol error. Unknown packet: %c got: %c\n", header.status);
+         return -1;
+      }
+   }
+
+   return -1;
+}
+
+/**
+ * @brief Handles a receive (read) packet header.
+ *
+ * @param ctx bpContext - for Bacula debug jobinfo messages
+ * @param cmd
+ * @return int32_t
+ */
+int32_t PTCOMM::handle_read_header(bpContext *ctx, char cmd)
+{
+   // first read is the packet header where we will have info about data
+   // which is sent to us; the packet header is 8 chars/bytes length fixed
+   // nbytes shows how many bytes we expects to read
+   int32_t length = recvbackend_header(ctx, cmd);
+   if (length < 0)
+   {
+      // error
+      DMSG0(ctx, DERROR, "PTCOMM cannot get packet header from backend.\n");
+      JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "PTCOMM cannot get packet header from backend.\n");
+      f_eod = f_error = f_fatal = true;
+      return -1;
+   }
+
+   return length;
+}
+
+/**
+ * @brief Handles a payload (message) which comes after the header.
+ *
+ * @param ctx bpContext - for Bacula debug jobinfo messages
+ * @param buf - the POOLMEM buffer we will read data
+ * @param nbytes - the size of the fized buffer
+ * @return int32_t
+ *    0: when backend sent signal, i.e. EOD or Term
+ *    -1: when we've got any error; the function will report it to Bacula when
+ *        ctx is not NULL
+ *    <n>: the size of received message
+ */
+int32_t PTCOMM::handle_payload(bpContext *ctx, char *buf, int32_t nbytes)
+{
+   // handle raw data read as payload
+   if(!recvbackend_data(ctx, buf, nbytes))
+   {
+      // error
+      DMSG0(ctx, DERROR, "PTCOMM cannot get packet payload from backend.\n");
+      JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "PTCOMM cannot get packet payload from backend.\n");
+      f_eod = f_error = f_fatal = true;
+      return -1;
+   }
+
+   return nbytes;
+}
+
+/**
+ * @brief Receive a packet from the backend.
+ *
+ * The caller expects a packet of a particular type (`cmd`) and we return
+ * from function only when we will receive this kind of packet or get any error.
+ * The `buf` will be extended if message extent current buffer size.
+ *
+ * @param ctx bpContext - for Bacula debug jobinfo messages
+ * @param cmd the packet type expected
+ * @param buf the POOL_MEM buffer we will read data
+ * @return int32_t
+ *    0: when backend sent signal, i.e. EOD or Term
+ *    -1: when we've got any error; the function will report it to Bacula when
+ *        ctx is not NULL
+ *    <n>: the size of received message
+ */
+int32_t PTCOMM::recvbackend(bpContext *ctx, char cmd, POOL_MEM &buf)
+{
+   // handle header
+   int32_t length = handle_read_header(ctx, cmd);
+   if (length < 0)
+      return -1;
+
+   // handle data payload
+   if (length > 0)
+   {
+      // check requested buffer size
+      buf.check_size(length);
+      return handle_payload(ctx, buf.c_str(), length);
+   }
+
+   return 0;
+}
+
+/**
+ * @brief Receive a packet from the backend.
+ *
+ * The caller expects a packet of a particular type (`cmd`) and we return
+ * from function when we will receive this kind of packet or get any error.
+ * The `buf` is fixed size, so it won't be extended for larger messages.
+ * In this case you have to make more calls to get all data.
+ *
+ * @param ctx bpContext - for Bacula debug jobinfo messages
+ * @param cmd - the packet type expected
+ * @param buf - the POOLMEM buffer we will read data
+ * @param bufsize - the size of the fized buffer
+ * @return int32_t
+ *    0: when backend sent signal, i.e. EOD or Term
+ *    -1: when we've got any error; the function will report it to Bacula when
+ *        ctx is not NULL
+ *    <n>: the size of received message
+ */
+int32_t PTCOMM::recvbackend_fixed(bpContext *ctx, char cmd, char *buf, int32_t bufsize)
+{
+   int32_t length = remaininglen;
+
+   if (!f_cont)
+   {
+      // handle header
+      length = handle_read_header(ctx, cmd);
+      if (length < 0)
+         return -1;
+   }
+
+   // handle data payload
+   if (length > 0)
+   {
+      // we will need subsequent call to handle remaining data only when `buf` to short
+      f_cont = length > bufsize;
+      int32_t nbytes = f_cont * bufsize + (!f_cont) * length;
+      remaininglen = f_cont * (length - bufsize);
+      return handle_payload(ctx, buf, nbytes);
+   }
+
+   return 0;
+}
+
+/*
+ * Sends packet to the backend.
+ *    The protocol allows sending no more than 999999 bytes of data in one packet.
+ *    If you require to send more data you have to split it in more packages, and
+ *    backend has to assemble it into a larger chunk of data.
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    cmd - the packet status to send
+ *    buf - the packet contents
+ *    len - the length of the contents
+ * out:
+ *    -1 - when encountered any error
+ *    <n> - the number of bytes sent, success
+ */
+int32_t PTCOMM::sendbackend(bpContext *ctx, char cmd, POOLMEM *buf, int32_t len)
+{
+   int status;
+   PTHEADER *header;
+   PTHEADER myheader;
+
+   if (is_closed()){
+      DMSG0(ctx, DERROR, "BPIPE to backend is closed, cannot send data.\n");
+      JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE to backend is closed, cannot send data.\n");
+      return -1;
+   }
+
+   if (len > 999999){
+      /* message length too long, cannot send it */
+      DMSG(ctx, DERROR, "Message length %i too long, cannot send data.\n", len);
+      JMSG(ctx, M_FATAL, "Message length %i too long, cannot send data.\n", len);
+      return -1;
+   }
+
+#ifdef NEED_REVIEW
+   // The code at NEED_REVIEW uses POOLMEM abufhead reserved space for
+   // packet header rendering in the same way as bsock.c do. The code was tested
+   // and is working fine. No memory leakage or corruption encountered.
+   // The only pros for this code is a single fwrite call for a whole message
+   // instead of two fwrites (header + data) for a standard method.
+   if (buf){
+      // we will prepare POOLMEM for sending data so we can render header here
+      header = (PTHEADER*) (buf - sizeof(PTHEADER));
+   } else {
+      // we will send header only
+      header = &myheader;
+   }
+#else
+   header = &myheader;
+#endif
+   header->status = cmd;
+   DMSG2(ctx, DDEBUG, "SENT: %c %s\n", header->status, buf ? buf : "");
+   if (bsnprintf(header->length, sizeof(PTHEADER), "%06i", len) != 6){
+      /* problem rendering packet header */
+      DMSG0(ctx, DERROR, "Problem rendering packet header for command.\n");
+      JMSG0(ctx, M_FATAL, "Problem rendering packet header for command.\n");
+      return -1;
+   }
+   header->length[6] = '\n';
+
+#ifdef NEED_REVIEW
+   status = sendbackend_data(ctx, (char*)header, len + sizeof(PTHEADER));
+   status -= sizeof(PTHEADER);
+#else
+   status = write(wfd, header, sizeof(PTHEADER));
+   if (buf){
+      /* we have some data or command to send */
+      status = write(wfd, buf, len);
+   }
+#endif
+   if (status < 0)
+   {
+      // error
+      DMSG0(ctx, DERROR, "PTCOMM cannot write packet to backend.\n");
+      JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "PTCOMM cannot write packet to backend.\n");
+      f_eod = f_error = f_fatal = true;
+      return -1;
+   }
+
+#ifdef NEED_REVIEW
+   // correct real payload data size
+   status -= sizeof(PTHEADER);
+#endif
+
+   return status;
+}
+
+/**
+ * @brief Reads the next command message from the backend communication channel.
+ *
+ * It expects the command, so returned data will be null terminated string
+ * stripped on any unwanted junk, i.e. '\n' or 'space'.
+ *
+ * @param ctx bpContext - for Bacula debug and jobinfo messages
+ * @param buf buffer allocated for command
+ * @return int32_t
+ *    -1 - when encountered any error
+ *    0 - when backend sent signal, i.e. EOD or Term
+ *    <n> - the number of bytes received, success
+ */
+int32_t PTCOMM::read_command(bpContext *ctx, POOL_MEM &buf)
+{
+   int32_t status = recvbackend(ctx, 'C', buf);
+   if (status > 0)
+   {
+      /* mark end of string because every command is a string */
+      buf.c_str()[status] = '\0';
+      /* strip any junk in command like '\n' or trailing spaces */
+      strip_trailing_junk(buf.c_str());
+   }
+
+   return status;
+}
+
+/*
+ * Reads the next data message from the backend.
+ *    The number of bytes received will not exceed the buffer length even when
+ *    backend will send more data. In this case next call to read_data() will
+ *    return the next part of the message.
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    buf - buffer allocated for data
+ *    len - the size of the allocated buffer
+ * out:
+ *    -1 - when encountered any error
+ *    0 - when backend sent signal, i.e. EOD or Term
+ *    <n> - the number of bytes received, success
+ *    buf - the command string received from backend
+ */
+int32_t PTCOMM::read_data(bpContext *ctx, POOL_MEM &buf)
+{
+   int32_t status;
+
+   if (extpipe > 0){
+      status = read(extpipe, buf.c_str(), buf.size());
+   } else {
+      status = recvbackend(ctx, 'D', buf);
+   }
+
+   return status;
+}
+
+/*
+ * Reads the next data message from the backend.
+ *    The number of bytes received will not exceed the buffer length even when
+ *    backend will send more data. In this case next call to read_data() will
+ *    return the next part of the message.
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    buf - buffer allocated for data
+ *    len - the size of the allocated buffer
+ * out:
+ *    -1 - when encountered any error
+ *    0 - when backend sent signal, i.e. EOD or Term
+ *    <n> - the number of bytes received, success
+ *    buf - the command string received from backend
+ */
+int32_t PTCOMM::read_data_fixed(bpContext *ctx, char *buf, int32_t len)
+{
+   int32_t status;
+
+   if (extpipe > 0){
+      status = read(extpipe, buf, len);
+   } else {
+      status = recvbackend_fixed(ctx, 'D', buf, len);
+   }
+
+   return status;
+}
+
+/*
+ * Receive an acknowledge from backend (the EOD package).
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ * out:
+ *    True when acknowledge received
+ *    False when got any error
+ */
+bool PTCOMM::read_ack(bpContext *ctx)
+{
+   POOL_MEM buf(PM_FNAME);
+
+   if (recvbackend(ctx, 'F', buf) == 0 && f_eod)
+   {
+      f_eod = false;
+      return true;
+   }
+
+   return false;
+}
+
+/*
+ * Sends a command to the backend.
+ *    The command has to be a nul terminated string.
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    buf - a message buffer contains command to send
+ * out:
+ *    -1 - when encountered any error
+ *    <n> - the number of bytes sent, success
+ */
+int32_t PTCOMM::write_command(bpContext *ctx, POOLMEM *buf)
+{
+   int32_t len;
+   len = buf ? strlen(buf) : 0;
+   return sendbackend(ctx, 'C', buf, len);
+}
+
+/*
+ * Sends a raw data to backend.
+ *    The length of the data should not exceed max packet size which is 999999 Byes.
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ *    buf - a message buffer contains data to send
+ *    len - the length of the data to send
+ * out:
+ *    -1 - when encountered any error
+ *    <n> - the number of bytes sent, success
+ */
+int32_t PTCOMM::write_data(bpContext *ctx, POOLMEM *buf, int32_t len)
+{
+   int32_t status;
+
+   if (extpipe > 0){
+      status = write(extpipe, buf, len);
+   } else {
+      status = sendbackend(ctx, 'D', buf, len);
+   }
+   return status;
+}
+
+/*
+ * Sends acknowledge to the backend which consist of the following flow:
+ *    -> EOD
+ *    <- OK
+ *    or
+ *    <- Error
+ *
+ * in:
+ *    bpContext - for Bacula debug and jobinfo messages
+ * out:
+ *    True when acknowledge sent successful
+ *    False when got any error
+ */
+bool PTCOMM::send_ack(bpContext *ctx)
+{
+   POOL_MEM buf(PM_FNAME);
+
+   if (signal_eod(ctx) < 0){
+      // error
+      return false;
+   }
+
+   if (read_command(ctx, buf) < 0){
+      // error
+      return false;
+   }
+
+   // check if backend response with OK
+   if (bstrcmp(buf.c_str(), "OK")){
+      // great ACk confirmed
+      return true;
+   }
+
+   return false;
+}
+
+/**
+ * @brief Send a handshake procedure to the backend using PLUGINNAME and PLUGINAPI.
+ *
+ * @param ctx bpContext - for Bacula debug and jobinfo messages
+ * @param pluginname - the plugin name part of the handshake
+ * @param pluginapi - the protocol version of the plugin
+ * @return true - when handshake successful
+ * @return false - when not
+ */
+bool PTCOMM::handshake(bpContext *ctx, const char *pluginname, const char * pluginapi)
+{
+   POOL_MEM cmd(PM_FNAME);
+
+   Mmsg(cmd, "Hello %s %s\n", pluginname, pluginapi);
+   int32_t status = write_command(ctx, cmd);
+   if (status > 0){
+      status = read_command(ctx, cmd);
+      if (status > 0){
+         if (bstrcmp(cmd.c_str(), "Hello Bacula")){
+            /* handshake successful */
+            return true;
+         } else {
+            DMSG(ctx, DERROR, "Wrong backend response to Hello command, got: %s\n", cmd.c_str());
+            JMSG(ctx, is_fatal() ? M_FATAL : M_ERROR, "Wrong backend response to Hello command, got: %s\n", cmd.c_str());
+         }
+      }
+   }
+
+   return false;
+}
diff --git a/bacula/src/plugins/fd/pluginlib/ptcomm.h b/bacula/src/plugins/fd/pluginlib/ptcomm.h
new file mode 100644 (file)
index 0000000..515c680
--- /dev/null
@@ -0,0 +1,286 @@
+/*
+   Bacula® - The Network Backup Solution
+
+   Copyright (C) 2007-2017 Bacula Systems SA
+   All rights reserved.
+
+   The main author of Bacula is Kern Sibbald, with contributions from many
+   others, a complete list can be found in the file AUTHORS.
+
+   Licensees holding a valid Bacula Systems SA license may use this file
+   and others of this release in accordance with the proprietary license
+   agreement provided in the LICENSE file.  Redistribution of any part of
+   this release is not permitted.
+
+   Bacula® is a registered trademark of Kern Sibbald.
+*/
+/**
+ * This is a process communication lowlevel library for Bacula plugin.
+ * Author: Radoslaw Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o.
+ */
+
+#ifndef _PTCOMM_H_
+#define _PTCOMM_H_
+
+#include "pluginlib.h"
+
+#define PTCOMM_DEFAULT_TIMEOUT   300        // timeout waiting for data 15 min, it should be enough
+
+/*
+ * The protocol packet header.
+ *  Every packet exchanged between Plugin and Backend will have a special header
+ *  which allow to perfectly synchronize data exchange mitigating the risk
+ *  of a deadlock, where both ends will wait for a data and no one wants to
+ *  send it to the other end.
+ *  The protocol implements a single char packet status which could be:
+ *      D - data packet
+ *      C - command packet
+ *      E - error packet
+ *      F - EOD packet
+ *      T - terminate connection
+ *      W - warning message
+ *      I - information message
+ *      A - fatal error message (abort)
+ *  The length is an ascii coded decimal trailed by a "newline" char - '\n'.
+ *  So, a packet header could be rendered as: 'C000012\n'
+ */
+struct PTHEADER
+{
+   char status;
+   char length[7];
+};
+
+/*
+ * This is a low-level transport communication class which handles all bits and
+ * bytes of the protocol.
+ *  The class express a high-level methods for low-level transport protocol.
+ *  It handles a communication channel (bpipe) with backend execution and
+ *  termination. The external data exchange using named pipes or local files is
+ *  handled by this class too.
+ */
+class PTCOMM : public SMARTALLOC
+{
+private:
+   BPIPE *bpipe;              // this is our bpipe to communicate with backend */
+   int rfd;                   //
+   int wfd;                   //
+   int efd;                   //
+   int maxfd;                 //
+   POOL_MEM errmsg;           // message buffer for error string */
+   int extpipe;               // set when data blast is performed using external pipe/file */
+   POOL_MEM extpipename;      // name of the external pipe/file for restore */
+   bool f_eod;                // the backend signaled EOD */
+   bool f_error;              // the backend signaled an error */
+   bool f_fatal;              // the backend signaled a fatal error */
+   bool f_cont;               // when we are reading next part of data packet */
+   bool abort_on_error;       // abort on error flag */
+   int32_t remaininglen;      // the number of bytes to read when `f_cont` is true
+   struct timeval _timeout;   //
+
+protected:
+   bool recvbackend_data(bpContext *ctx, char *buf, int32_t nbytes);
+   bool sendbackend_data(bpContext *ctx, char *buf, int32_t nbytes);
+
+   int32_t recvbackend_header(bpContext *ctx, char cmd);
+   int32_t handle_read_header(bpContext *ctx, char cmd);
+   int32_t handle_payload(bpContext *ctx, char *buf, int32_t nbytes);
+
+   int32_t recvbackend(bpContext *ctx, char cmd, POOL_MEM &buf);
+   int32_t recvbackend_fixed(bpContext *ctx, char cmd, char *buf, int32_t bufsize);
+
+   int32_t sendbackend(bpContext *ctx, char cmd, POOLMEM *buf, int32_t len);
+
+public:
+   PTCOMM() :
+      bpipe(NULL),
+      rfd(0),
+      wfd(0),
+      efd(0),
+      maxfd(0),
+      errmsg(PM_MESSAGE),
+      extpipe(-1),
+      extpipename(PM_FNAME),
+      f_eod(false),
+      f_error(false),
+      f_fatal(false),
+      f_cont(false),
+      abort_on_error(false),
+      remaininglen(0)
+   {}
+#if __cplusplus > 201103L
+   PTCOMM(PTCOMM &) = delete;
+   PTCOMM(PTCOMM &&) = delete;
+#endif
+   ~PTCOMM() { terminate(NULL); }
+
+   bool handshake(bpContext *ctx, const char *pluginname, const char *pluginapi);
+
+   int32_t read_command(bpContext *ctx, POOL_MEM &buf);
+   int32_t read_data(bpContext *ctx, POOL_MEM &buf);
+   int32_t read_data_fixed(bpContext *ctx, char *buf, int32_t len);
+
+   int32_t write_command(bpContext *ctx, char *buf);
+
+   /**
+    * @brief Sends a command to the backend.
+    *
+    * @param ctx bpContext - for Bacula debug and jobinfo messages
+    * @param buf a message buffer contains command to send
+    * @return int32_t
+    *    -1 - when encountered any error
+    *    <n> - the number of bytes sent, success
+    */
+   int32_t write_command(bpContext *ctx, POOL_MEM &buf) { return write_command(ctx, buf.addr()); }
+   int32_t write_data(bpContext *ctx, char *buf, int32_t len);
+
+   bool read_ack(bpContext *ctx);
+   bool send_ack(bpContext *ctx);
+
+   /**
+    * @brief Signals en error to the backend.
+    *
+    * The buf, when not NULL, can hold an error string sent do the backend.
+    *
+    * @param ctx - for Bacula debug and jobinfo messages
+    * @param buf - when not NULL should consist of an error string
+    *            - when NULL, no error string sent to the backend
+    * @return int32_t
+    *            -1 - when encountered any error
+    *            <n> - the number of bytes sent, success
+    */
+   inline int32_t signal_error(bpContext *ctx, POOLMEM *buf)
+   {
+      int32_t len = buf ? strlen(buf) : 0;
+      return sendbackend(ctx, 'E', buf, len);
+   }
+
+   POOLMEM *get_error(bpContext *ctx);
+
+   /**
+    * @brief Signals EOD to backend.
+    *
+    * @param ctx bpContext - for Bacula debug and jobinfo messages
+    * @return int32_t
+    *    -1 - when encountered any error
+    *    <n> - the number of bytes sent, success
+    */
+   inline int32_t signal_eod(bpContext *ctx) { return sendbackend(ctx, 'F', NULL, 0); }
+
+   /**
+    * @brief Signal end of communication to the backend.
+    *    The backend should close the connection after receiving this packet.
+    *
+    * @param ctx bpContext - for Bacula debug and jobinfo messages
+    * @return int32_t
+    *    -1 - when encountered any error
+    *    <n> - the number of bytes sent, success
+    */
+   inline int32_t signal_term(bpContext *ctx) { return sendbackend(ctx, 'T', NULL, 0); }
+
+   void terminate(bpContext *ctx);
+
+   /**
+    * @brief Returns a backend PID if available.
+    *    I'm using an arthrymetic way to make a conditional value return;
+    *
+    * @return int backend PID - when backend available; -1 - when backend is unavailable
+    */
+   int get_backend_pid() { return (bpipe != NULL) * bpipe->worker_pid - (bpipe == NULL); }
+
+   /**
+    * @brief Sets a BPIPE object for our main communication channel.
+    *
+    * @param bp object, we do not check for NULL here
+    */
+   inline void set_bpipe(BPIPE *bp)
+   {
+      bpipe = bp;
+      rfd = fileno(bpipe->rfd);
+      wfd = fileno(bpipe->wfd);
+      efd = fileno(bpipe->efd);
+      maxfd = MAX(rfd, wfd);
+      maxfd = MAX(maxfd, efd) + 1;
+   }
+
+   /**
+    * @brief Sets a FILE descriptor used as external pipe during backup and restore.
+    *
+    * @param ep a FILE* descriptor used during
+    */
+   inline void set_extpipe(int ep) { extpipe = ep; }
+
+   /**
+    * @brief Sets an external pipe name for restore.
+    *
+    * @param epname - external pipe name
+    */
+   inline void set_extpipename(char *epname) { pm_strcpy(extpipename, epname); }
+   bool close_extpipe(bpContext *ctx);
+
+   /**
+    * @brief Checks if connection is open and we can use a bpipe object for communication.
+    *
+    * @return true if connection is available
+    * @return false if connection is closed and we can't use bpipe object
+    */
+   inline bool is_open() { return bpipe != NULL; }
+
+   /**
+    * @brief Checks if connection is closed and we can't use a bpipe object for communication.
+    *
+    * @return true if connection is closed and we can't use bpipe object
+    * @return false if connection is available
+    */
+   inline bool is_closed() { return bpipe == NULL; }
+
+   /**
+    * @brief Checks if backend sent us some error, backend error message is flagged on f_error.
+    *
+    * @return true when last packet was an error
+    * @return false when no error packets was received
+    */
+   inline bool is_error() { return f_error || f_fatal; }
+
+   /**
+    * @brief Checks if backend sent us fatal error, backend error message is flagged on f_fatal.
+    *
+    * @return true when last packet was a fatal error
+    * @return false when no fatal error packets was received
+    */
+   inline bool is_fatal() { return f_fatal || (f_error && abort_on_error); }
+
+   /**
+    * @brief Checks if backend signaled EOD, eod from backend is flagged on f_eod.
+    *
+    * @return true when backend signaled EOD on last packet
+    * @return false when backend did not signal EOD
+    */
+   inline bool is_eod() { return f_eod; }
+
+   /**
+    * @brief Clears the EOD from backend flag, f_eod.
+    *    The eod flag is set when EOD message received from backend and not cleared
+    *    until next recvbackend() call.
+    */
+   void clear_eod() { f_eod = false; }
+
+   /**
+    * @brief Set the abort on error flag
+    */
+   inline void set_abort_on_error() { abort_on_error = true; }
+
+   /**
+    * @brief Clears the abort on error flag.
+    */
+   inline void clear_abort_on_error() { abort_on_error = false; }
+
+   /**
+    * @brief return abort on error flag status
+    *
+    * @return true if flag is set
+    * @return false  if flag is not set
+    */
+   bool is_abort_on_error() { return abort_on_error; }
+};
+
+#endif   /* _PTCOMM_H_ */