Explorar o código

- New API (with default configuration options)
- API test
- ProcessorConstructor has to be public
- moved witePid to go-guerrilla package
- move backend reloading to go-guerrilla package
- fixed race condition when reloading backend
- fixed race condition when making servers

flashmob %!s(int64=8) %!d(string=hai) anos
pai
achega
4b9a781368

+ 159 - 122
api.go

@@ -1,171 +1,208 @@
 package guerrilla
 package guerrilla
 
 
 import (
 import (
+	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	_ "fmt"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/log"
 	"io/ioutil"
 	"io/ioutil"
-	"os"
+	"time"
 )
 )
 
 
-type SMTP struct {
-	config  *AppConfig
-	logger  log.Logger
-	backend backends.Backend
-	g       Guerrilla
+type Daemon struct {
+	Config  *AppConfig
+	Logger  log.Logger
+	Backend backends.Backend
+
+	g Guerrilla
+
+	configLoadTime time.Time
 }
 }
 
 
 const defaultInterface = "127.0.0.1:2525"
 const defaultInterface = "127.0.0.1:2525"
 
 
-// configureDefaults fills in default server settings for values that were not configured
-func (s *SMTP) configureDefaults() error {
-	if s.config.LogFile == "" {
-		s.config.LogFile = log.OutputStderr.String()
-	}
-	if s.config.LogLevel == "" {
-		s.config.LogLevel = "debug"
-	}
-	if len(s.config.AllowedHosts) == 0 {
-		if h, err := os.Hostname(); err != nil {
+// AddProcessor adds a processor constructor to the backend.
+// name is the identifier to be used in the config. See backends docs for more info.
+func (d *Daemon) AddProcessor(name string, pc backends.ProcessorConstructor) {
+	backends.Svc.AddProcessor(name, pc)
+}
+
+// Starts the daemon, initializing d.Config, d.Logger and d.Backend with defaults
+// can only be called once through the lifetime of the program
+func (d *Daemon) Start() (err error) {
+	if d.g == nil {
+		if d.Config == nil {
+			d.Config = &AppConfig{}
+		}
+		if err = d.configureDefaults(); err != nil {
 			return err
 			return err
-		} else {
-			s.config.AllowedHosts = append(s.config.AllowedHosts, h)
 		}
 		}
-	}
-	h, err := os.Hostname()
-	if err != nil {
-		return err
-	}
-	if len(s.config.Servers) == 0 {
-		sc := ServerConfig{}
-		sc.LogFile = s.config.LogFile
-		sc.ListenInterface = defaultInterface
-		sc.IsEnabled = true
-		sc.Hostname = h
-		sc.MaxClients = 100
-		sc.Timeout = 30
-		sc.MaxSize = 10 << 20 // 10 Mebibytes
-		s.config.Servers = append(s.config.Servers, sc)
-	} else {
-		// make sure each server has defaults correctly configured
-		for i := range s.config.Servers {
-			if s.config.Servers[i].Hostname == "" {
-				s.config.Servers[i].Hostname = h
-			}
-			if s.config.Servers[i].MaxClients == 0 {
-				s.config.Servers[i].MaxClients = 100
-			}
-			if s.config.Servers[i].Timeout == 0 {
-				s.config.Servers[i].Timeout = 20
-			}
-			if s.config.Servers[i].MaxSize == 0 {
-				s.config.Servers[i].MaxSize = 10 << 20 // 10 Mebibytes
-			}
-			if s.config.Servers[i].ListenInterface == "" {
-				return errors.New(fmt.Sprintf("Listen interface not specified for server at index %d", i))
-			}
-			if s.config.Servers[i].LogFile == "" {
-				s.config.Servers[i].LogFile = s.config.LogFile
+		if d.Logger == nil {
+			d.Logger, err = log.GetLogger(d.Config.LogFile)
+			if err != nil {
+				return err
 			}
 			}
-			// validate the server config
-			err = s.config.Servers[i].Validate()
+			d.Logger.SetLevel(d.Config.LogLevel)
+		}
+		if d.Backend == nil {
+			d.Backend, err = backends.New(d.Config.BackendConfig, d.Logger)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
 		}
 		}
+		d.g, err = New(d.Config, d.Backend, d.Logger)
+		if err != nil {
+			return err
+		}
+	}
+	err = d.g.Start()
+	if err == nil {
+		if err := d.resetLogger(); err == nil {
+			d.log().Infof("main log configured to %s", d.Config.LogFile)
+		}
 
 
 	}
 	}
-	return nil
+	return err
+}
 
 
+// Shuts down the daemon, including servers and backend.
+// Do not call Start on it again, use a new server.
+func (d *Daemon) Shutdown() {
+	if d.g != nil {
+		d.g.Shutdown()
+	}
 }
 }
 
 
-func (s *SMTP) configureDefaultBackend() error {
-	h, err := os.Hostname()
+// ReadConfig reads in the config from a JSON file.
+func (d *Daemon) ReadConfig(path string) error {
+	data, err := ioutil.ReadFile(path)
 	if err != nil {
 	if err != nil {
+		return fmt.Errorf("Could not read config file: %s", err.Error())
+	}
+	d.Config = &AppConfig{}
+	if err := d.Config.Load(data); err != nil {
 		return err
 		return err
 	}
 	}
-	if len(s.config.BackendConfig) == 0 {
-		bcfg := backends.BackendConfig{
-			"log_received_mails": true,
-			"save_workers_size":  1,
-			"process_stack":      "HeadersParser|Header|Debugger",
-			"primary_mail_host":  h,
-		}
-		s.backend, err = backends.New(bcfg, s.logger)
-		if err != nil {
-			return err
-		}
+	d.configLoadTime = time.Now()
+	return nil
+}
+
+// SetConfig is same as ReadConfig, except you can pass AppConfig directly
+func (d *Daemon) SetConfig(c AppConfig) error {
+	// Config.Load takes []byte so we need to serialize
+	data, err := json.Marshal(c)
+	if err != nil {
+		return err
+	}
+	// put the data into a fresh d.Config
+	d.Config = &AppConfig{}
+	if err := d.Config.Load(data); err != nil {
+		return err
+	}
+	d.configLoadTime = time.Now()
+	return nil
+}
+
+// Reload a config using the passed in AppConfig and emit config change events
+func (d *Daemon) ReloadConfig(c AppConfig) error {
+	if d.Config == nil {
+		return errors.New("d.Config nil")
+	}
+	oldConfig := *d.Config
+	err := d.SetConfig(c)
+	if err != nil {
+		d.log().WithError(err).Error("Error while reloading config")
+		return err
 	} else {
 	} else {
-		if _, ok := s.config.BackendConfig["process_stack"]; !ok {
-			s.config.BackendConfig["process_stack"] = "HeadersParser|Header|Debugger"
-		}
-		if _, ok := s.config.BackendConfig["primary_mail_host"]; !ok {
-			s.config.BackendConfig["primary_mail_host"] = h
-		}
-		if _, ok := s.config.BackendConfig["save_workers_size"]; !ok {
-			s.config.BackendConfig["save_workers_size"] = 1
-		}
+		d.log().Infof("Configuration was reloaded at %s", d.configLoadTime)
+		d.Config.EmitChangeEvents(&oldConfig, d.g)
+	}
+	return nil
+}
 
 
-		if _, ok := s.config.BackendConfig["log_received_mails"]; !ok {
-			s.config.BackendConfig["log_received_mails"] = false
-		}
-		s.backend, err = backends.New(s.config.BackendConfig, s.logger)
-		if err != nil {
-			return err
-		}
+// Reload a config from a file and emit config change events
+func (d *Daemon) ReloadConfigFile(path string) error {
+	if d.Config == nil {
+		return errors.New("d.Config nil")
 	}
 	}
+	var oldConfig AppConfig
+	oldConfig = *d.Config
+	err := d.ReadConfig(path)
 
 
+	if err != nil {
+		d.log().WithError(err).Error("Error while reloading config from file")
+		return err
+	} else {
+		d.log().Infof("Configuration was reloaded at %s", d.configLoadTime)
+		d.Config.EmitChangeEvents(&oldConfig, d.g)
+	}
 	return nil
 	return nil
 }
 }
 
 
-func (s *SMTP) Start() (err error) {
-	if s.g == nil {
-		if s.config == nil {
-			s.config = &AppConfig{}
-		}
-		err = s.configureDefaults()
-		if err != nil {
-			return err
-		}
+// ReopenLogs re-opens all log files. Typically, one would call this after rotating logs
+func (d *Daemon) ReopenLogs() {
+	d.Config.EmitLogReopenEvents(d.g)
+}
 
 
-		if s.logger == nil {
-			s.logger, err = log.GetLogger(s.config.LogFile)
-			if err != nil {
-				return err
-			}
-		}
-		if s.backend == nil {
-			err = s.configureDefaultBackend()
-			if err != nil {
-				return
-			}
-		}
-		s.g, err = New(s.config, s.backend, s.logger)
-		if err != nil {
-			return err
-		}
+// Subscribe for subscribing to config change events
+func (d *Daemon) Subscribe(topic Event, fn interface{}) error {
+	return d.g.Subscribe(topic, fn)
+}
 
 
-	}
-	return s.g.Start()
+// for publishing config change events
+func (d *Daemon) Publish(topic Event, args ...interface{}) {
+	d.g.Publish(topic, args...)
 }
 }
 
 
-func (s *SMTP) Shutdown() {
-	s.g.Shutdown()
+// for unsubscribing from config change events
+func (d *Daemon) Unsubscribe(topic Event, handler interface{}) error {
+	return d.g.Unsubscribe(topic, handler)
 }
 }
 
 
-// ReadConfig reads in the config from a json file.
-func (s *SMTP) ReadConfig(path string) error {
-	data, err := ioutil.ReadFile(path)
+// log returns a logger that implements our log.Logger interface.
+// level is set to "info" by default
+func (d *Daemon) log() log.Logger {
+	if d.Logger != nil {
+		return d.Logger
+	}
+	out := log.OutputStderr.String()
+	if d.Config != nil && len(d.Config.LogFile) > 0 {
+		out = d.Config.LogFile
+	}
+	l, err := log.GetLogger(out)
+	if err == nil {
+		l.SetLevel("info")
+	}
+	return l
+
+}
+
+// set the default values for the servers and backend config options
+func (d *Daemon) configureDefaults() error {
+	err := d.Config.setDefaults()
 	if err != nil {
 	if err != nil {
-		return fmt.Errorf("Could not read config file: %s", err.Error())
+		return err
 	}
 	}
-	if s.config == nil {
-		s.config = &AppConfig{}
+	if d.Backend == nil {
+		err = d.Config.setBackendDefaults()
+		if err != nil {
+			return err
+		}
 	}
 	}
-	if err := s.config.Load(data); err != nil {
+	return err
+}
+
+// resetLogger sets the logger to the one specified in the config.
+// This is because at the start, the daemon may be logging to stderr,
+// then attaches to the logs once the config is loaded.
+// This will propagate down to the servers / backend too.
+func (d *Daemon) resetLogger() error {
+	l, err := log.GetLogger(d.Config.LogFile)
+	if err != nil {
 		return err
 		return err
 	}
 	}
+	d.Logger = l
+	d.g.SetLogger(d.Logger)
 	return nil
 	return nil
 }
 }

+ 73 - 37
api_test.go

@@ -11,29 +11,29 @@ import (
 // Test Starting smtp without setting up logger / backend
 // Test Starting smtp without setting up logger / backend
 func TestSMTP(t *testing.T) {
 func TestSMTP(t *testing.T) {
 
 
-	smtp := SMTP{}
-	err := smtp.Start()
+	d := Daemon{}
+	err := d.Start()
 
 
 	if err != nil {
 	if err != nil {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
 	// it should set to stderr automatically
 	// it should set to stderr automatically
-	if smtp.config.LogFile != log.OutputStderr.String() {
+	if d.Config.LogFile != log.OutputStderr.String() {
 		t.Error("smtp.config.LogFile is not", log.OutputStderr.String())
 		t.Error("smtp.config.LogFile is not", log.OutputStderr.String())
 	}
 	}
 
 
-	if len(smtp.config.AllowedHosts) == 0 {
-		t.Error("smtp.config.AllowedHosts len should be 1, not 0", smtp.config.AllowedHosts)
+	if len(d.Config.AllowedHosts) == 0 {
+		t.Error("smtp.config.AllowedHosts len should be 1, not 0", d.Config.AllowedHosts)
 	}
 	}
 
 
-	if smtp.config.LogLevel != "debug" {
-		t.Error("smtp.config.LogLevel expected'debug', it is", smtp.config.LogLevel)
+	if d.Config.LogLevel != "debug" {
+		t.Error("smtp.config.LogLevel expected'debug', it is", d.Config.LogLevel)
 	}
 	}
-	if len(smtp.config.Servers) != 1 {
-		t.Error("len(smtp.config.Servers) should be 1, got", len(smtp.config.Servers))
+	if len(d.Config.Servers) != 1 {
+		t.Error("len(smtp.config.Servers) should be 1, got", len(d.Config.Servers))
 	}
 	}
 	time.Sleep(time.Second * 2)
 	time.Sleep(time.Second * 2)
-	smtp.Shutdown()
+	d.Shutdown()
 
 
 }
 }
 
 
@@ -42,7 +42,7 @@ func TestSMTPNoLog(t *testing.T) {
 
 
 	// configure a default server with no log output
 	// configure a default server with no log output
 	cfg := &AppConfig{LogFile: log.OutputOff.String()}
 	cfg := &AppConfig{LogFile: log.OutputOff.String()}
-	smtp := SMTP{config: cfg}
+	smtp := Daemon{Config: cfg}
 
 
 	err := smtp.Start()
 	err := smtp.Start()
 	if err != nil {
 	if err != nil {
@@ -60,7 +60,7 @@ func TestSMTPCustomServer(t *testing.T) {
 		IsEnabled:       true,
 		IsEnabled:       true,
 	}
 	}
 	cfg.Servers = append(cfg.Servers, sc)
 	cfg.Servers = append(cfg.Servers, sc)
-	smtp := SMTP{config: cfg}
+	smtp := Daemon{Config: cfg}
 
 
 	err := smtp.Start()
 	err := smtp.Start()
 	if err != nil {
 	if err != nil {
@@ -84,16 +84,17 @@ func TestSMTPCustomBackend(t *testing.T) {
 		"save_workers_size":  3,
 		"save_workers_size":  3,
 		"process_stack":      "HeadersParser|Header|Hasher|Debugger",
 		"process_stack":      "HeadersParser|Header|Hasher|Debugger",
 		"log_received_mails": true,
 		"log_received_mails": true,
+		"primary_mail_host":  "example.com",
 	}
 	}
 	cfg.BackendConfig = bcfg
 	cfg.BackendConfig = bcfg
-	smtp := SMTP{config: cfg}
+	d := Daemon{Config: cfg}
 
 
-	err := smtp.Start()
+	err := d.Start()
 	if err != nil {
 	if err != nil {
 		t.Error("start error", err)
 		t.Error("start error", err)
 	} else {
 	} else {
 		time.Sleep(time.Second * 2)
 		time.Sleep(time.Second * 2)
-		smtp.Shutdown()
+		d.Shutdown()
 	}
 	}
 }
 }
 
 
@@ -102,7 +103,35 @@ func TestSMTPLoadFile(t *testing.T) {
 	json := `{
 	json := `{
     "log_file" : "./tests/testlog",
     "log_file" : "./tests/testlog",
     "log_level" : "debug",
     "log_level" : "debug",
-    "pid_file" : "/var/run/go-guerrilla.pid",
+    "pid_file" : "tests/go-guerrilla.pid",
+    "allowed_hosts": ["spam4.me","grr.la"],
+    "backend_config" :
+        {
+            "log_received_mails" : true,
+            "process_stack": "HeadersParser|Header|Hasher|Debugger",
+            "save_workers_size":  3
+        },
+    "servers" : [
+        {
+            "is_enabled" : true,
+            "host_name":"mail.guerrillamail.com",
+            "max_size": 100017,
+            "private_key_file":"config_test.go",
+            "public_key_file":"config_test.go",
+            "timeout":160,
+            "listen_interface":"127.0.0.1:2526",
+            "start_tls_on":false,
+            "tls_always_on":false,
+            "max_clients": 2
+        }
+    ]
+}
+
+	`
+	json2 := `{
+    "log_file" : "./tests/testlog2",
+    "log_level" : "debug",
+    "pid_file" : "tests/go-guerrilla2.pid",
     "allowed_hosts": ["spam4.me","grr.la"],
     "allowed_hosts": ["spam4.me","grr.la"],
     "backend_config" :
     "backend_config" :
         {
         {
@@ -133,36 +162,43 @@ func TestSMTPLoadFile(t *testing.T) {
 		return
 		return
 	}
 	}
 
 
-	/*
-
-		cfg := &AppConfig{LogFile: log.OutputStdout.String()}
-		sc := ServerConfig{
-			ListenInterface: "127.0.0.1:2526",
-			IsEnabled:       true,
-		}
-		cfg.Servers = append(cfg.Servers, sc)
-		bcfg := backends.BackendConfig{
-			"save_workers_size":  3,
-			"process_stack":      "HeadersParser|Header|Hasher|Debugger",
-			"log_received_mails": true,
-		}
-		cfg.BackendConfig = bcfg
-
-		smtp := SMTP{config: cfg}
-	*/
-	smtp := SMTP{}
-	err = smtp.ReadConfig("goguerrilla.conf.api")
+	d := Daemon{}
+	err = d.ReadConfig("goguerrilla.conf.api")
 	if err != nil {
 	if err != nil {
 		t.Error("ReadConfig error", err)
 		t.Error("ReadConfig error", err)
 		return
 		return
 	}
 	}
 
 
-	err = smtp.Start()
+	err = d.Start()
 	if err != nil {
 	if err != nil {
 		t.Error("start error", err)
 		t.Error("start error", err)
 		return
 		return
 	} else {
 	} else {
 		time.Sleep(time.Second * 2)
 		time.Sleep(time.Second * 2)
-		smtp.Shutdown()
+		if d.Config.LogFile != "./tests/testlog" {
+			t.Error("d.Config.LogFile != \"./tests/testlog\"")
+		}
+
+		if d.Config.PidFile != "tests/go-guerrilla.pid" {
+			t.Error("d.Config.LogFile != tests/go-guerrilla.pid")
+		}
+
+		err := ioutil.WriteFile("goguerrilla.conf.api", []byte(json2), 0644)
+		if err != nil {
+			t.Error("could not write guerrilla.conf.api", err)
+			return
+		}
+
+		d.ReloadConfigFile("goguerrilla.conf.api")
+
+		if d.Config.LogFile != "./tests/testlog2" {
+			t.Error("d.Config.LogFile != \"./tests/testlog\"")
+		}
+
+		if d.Config.PidFile != "tests/go-guerrilla2.pid" {
+			t.Error("d.Config.LogFile != \"go-guerrilla.pid\"")
+		}
+
+		d.Shutdown()
 	}
 	}
 }
 }

+ 5 - 9
backends/backend.go

@@ -15,17 +15,17 @@ var (
 	Svc *service
 	Svc *service
 
 
 	// Store the constructor for making an new processor decorator.
 	// Store the constructor for making an new processor decorator.
-	processors map[string]processorConstructor
+	processors map[string]ProcessorConstructor
 
 
 	b Backend
 	b Backend
 )
 )
 
 
 func init() {
 func init() {
 	Svc = &service{}
 	Svc = &service{}
-	processors = make(map[string]processorConstructor)
+	processors = make(map[string]ProcessorConstructor)
 }
 }
 
 
-type processorConstructor func() Decorator
+type ProcessorConstructor func() Decorator
 
 
 // Backends process received mail. Depending on the implementation, they can store mail in the database,
 // Backends process received mail. Depending on the implementation, they can store mail in the database,
 // write to a file, check for spam, re-transmit to another server, etc.
 // write to a file, check for spam, re-transmit to another server, etc.
@@ -133,10 +133,6 @@ func convertError(name string) error {
 	return fmt.Errorf("failed to load backend config (%s)", name)
 	return fmt.Errorf("failed to load backend config (%s)", name)
 }
 }
 
 
-func GetBackend() Backend {
-	return b
-}
-
 type service struct {
 type service struct {
 	initializers []processorInitializer
 	initializers []processorInitializer
 	shutdowners  []processorShutdowner
 	shutdowners  []processorShutdowner
@@ -217,9 +213,9 @@ func (s *service) shutdown() Errors {
 // AddProcessor adds a new processor, which becomes available to the backend_config.process_stack option
 // AddProcessor adds a new processor, which becomes available to the backend_config.process_stack option
 // Use to add your own custom processor when using backends as a package, or after importing an external
 // Use to add your own custom processor when using backends as a package, or after importing an external
 // processor.
 // processor.
-func (s *service) AddProcessor(name string, p processorConstructor) {
+func (s *service) AddProcessor(name string, p ProcessorConstructor) {
 	// wrap in a constructor since we want to defer calling it
 	// wrap in a constructor since we want to defer calling it
-	var c processorConstructor
+	var c ProcessorConstructor
 	c = func() Decorator {
 	c = func() Decorator {
 		return p()
 		return p()
 	}
 	}

+ 10 - 10
backends/gateway.go

@@ -27,7 +27,7 @@ type BackendGateway struct {
 	// waits for backend workers to start/stop
 	// waits for backend workers to start/stop
 	wg           sync.WaitGroup
 	wg           sync.WaitGroup
 	workStoppers []chan bool
 	workStoppers []chan bool
-	lines        []Processor
+	chains       []Processor
 
 
 	// controls access to state
 	// controls access to state
 	sync.Mutex
 	sync.Mutex
@@ -180,19 +180,19 @@ func (gw *BackendGateway) Reinitialize() error {
 	return err
 	return err
 }
 }
 
 
-// newProcessorLine creates a new call-stack of decorators and returns as a single Processor
+// newChain creates a new Processor by chaining multiple Processors in a call stack
 // Decorators are functions of Decorator type, source files prefixed with p_*
 // Decorators are functions of Decorator type, source files prefixed with p_*
 // Each decorator does a specific task during the processing stage.
 // Each decorator does a specific task during the processing stage.
 // This function uses the config value process_stack to figure out which Decorator to use
 // This function uses the config value process_stack to figure out which Decorator to use
-func (gw *BackendGateway) newProcessorStack() (Processor, error) {
+func (gw *BackendGateway) newChain() (Processor, error) {
 	var decorators []Decorator
 	var decorators []Decorator
 	cfg := strings.ToLower(strings.TrimSpace(gw.gwConfig.ProcessorStack))
 	cfg := strings.ToLower(strings.TrimSpace(gw.gwConfig.ProcessorStack))
 	if len(cfg) == 0 {
 	if len(cfg) == 0 {
 		cfg = strings.ToLower(defaultProcessor)
 		cfg = strings.ToLower(defaultProcessor)
 	}
 	}
-	line := strings.Split(cfg, "|")
-	for i := range line {
-		name := line[len(line)-1-i] // reverse order, since decorators are stacked
+	items := strings.Split(cfg, "|")
+	for i := range items {
+		name := items[len(items)-1-i] // reverse order, since decorators are stacked
 		if makeFunc, ok := processors[name]; ok {
 		if makeFunc, ok := processors[name]; ok {
 			decorators = append(decorators, makeFunc())
 			decorators = append(decorators, makeFunc())
 		} else {
 		} else {
@@ -233,14 +233,14 @@ func (gw *BackendGateway) Initialize(cfg BackendConfig) error {
 			gw.State = BackendStateError
 			gw.State = BackendStateError
 			return errors.New("Must have at least 1 worker")
 			return errors.New("Must have at least 1 worker")
 		}
 		}
-		gw.lines = make([]Processor, 0)
+		gw.chains = make([]Processor, 0)
 		for i := 0; i < workersSize; i++ {
 		for i := 0; i < workersSize; i++ {
-			p, err := gw.newProcessorStack()
+			p, err := gw.newChain()
 			if err != nil {
 			if err != nil {
 				gw.State = BackendStateError
 				gw.State = BackendStateError
 				return err
 				return err
 			}
 			}
-			gw.lines = append(gw.lines, p)
+			gw.chains = append(gw.chains, p)
 		}
 		}
 		// initialize processors
 		// initialize processors
 		if err := Svc.initialize(cfg); err != nil {
 		if err := Svc.initialize(cfg); err != nil {
@@ -274,7 +274,7 @@ func (gw *BackendGateway) Start() error {
 			stop := make(chan bool)
 			stop := make(chan bool)
 			go func(workerId int, stop chan bool) {
 			go func(workerId int, stop chan bool) {
 				// blocks here until the worker exits
 				// blocks here until the worker exits
-				gw.workDispatcher(gw.conveyor, gw.lines[workerId], workerId+1, stop)
+				gw.workDispatcher(gw.conveyor, gw.chains[workerId], workerId+1, stop)
 				gw.wg.Done()
 				gw.wg.Done()
 			}(i, stop)
 			}(i, stop)
 			gw.workStoppers = append(gw.workStoppers, stop)
 			gw.workStoppers = append(gw.workStoppers, stop)

+ 5 - 5
backends/gateway_test.go

@@ -30,10 +30,10 @@ func TestInitialize(t *testing.T) {
 		t.Error("Gateway did not init because:", err)
 		t.Error("Gateway did not init because:", err)
 		t.Fail()
 		t.Fail()
 	}
 	}
-	if gateway.lines == nil {
-		t.Error("gateway.lines should not be nil")
-	} else if len(gateway.lines) != 1 {
-		t.Error("len(gateway.lines) should be 1, but got", len(gateway.lines))
+	if gateway.chains == nil {
+		t.Error("gateway.chains should not be nil")
+	} else if len(gateway.chains) != 1 {
+		t.Error("len(gateway.chains) should be 1, but got", len(gateway.chains))
 	}
 	}
 
 
 	if gateway.conveyor == nil {
 	if gateway.conveyor == nil {
@@ -82,7 +82,7 @@ func TestStartProcessStop(t *testing.T) {
 		MailFrom: mail.Address{User: "test", Host: "example.com"},
 		MailFrom: mail.Address{User: "test", Host: "example.com"},
 		TLS:      true,
 		TLS:      true,
 	}
 	}
-	e.PushRcpt(mail.Address{"test", "example.com"})
+	e.PushRcpt(mail.Address{User: "test", Host: "example.com"})
 	e.Data.WriteString("Subject:Test\n\nThis is a test.")
 	e.Data.WriteString("Subject:Test\n\nThis is a test.")
 	notify := make(chan *notifyMsg)
 	notify := make(chan *notifyMsg)
 
 

+ 1 - 1
backends/p_mysql.go

@@ -152,7 +152,7 @@ func MySql() Decorator {
 		mp.config = config
 		mp.config = config
 		db, err = mp.connect(config)
 		db, err = mp.connect(config)
 		if err != nil {
 		if err != nil {
-			Log().Error("cannot open mysql: %s", err)
+			Log().Errorf("cannot open mysql: %s", err)
 			return err
 			return err
 		}
 		}
 		return nil
 		return nil

+ 91 - 0
cmd/guerrillad/backend_test.go.no

@@ -0,0 +1,91 @@
+package main
+
+import (
+	"testing"
+	"os"
+	"time"
+	"io/ioutil"
+	"github.com/flashmob/go-guerrilla/tests/testcert"
+	"github.com/flashmob/go-guerrilla/log"
+	"runtime"
+	"github.com/spf13/cobra"
+	"sync"
+	"strings"
+	"fmt"
+)
+
+func TestBadBackendReload2(t *testing.T) {
+
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	os.Truncate("../../tests/testlog", 0)
+	//mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("stdout")
+	mainlog.SetLevel("debug")
+	mainlog.Info("are u sure")
+	mainlog.Info("not another word")
+
+	select {
+
+	case <-time.After(10 * time.Second):
+		mainlog.Info("paabix")
+		stacktrace := make([]byte, 8192)
+		length := runtime.Stack(stacktrace, true)
+		_ = length
+		fmt.Fprintf(ioutil.Discard, (string(stacktrace[:length])))
+
+		panic("timed out")
+	}
+
+	mainlog.Info("not another word")
+	sigKill()
+	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
+	cmd := &cobra.Command{}
+	configPath = "configJsonA.json"
+	var serveWG sync.WaitGroup
+	serveWG.Add(1)
+	go func() {
+		mainlog.Info("start serve")
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	mainlog.Info("after start")
+	time.Sleep(testPauseDuration)
+
+	// change the config file to the one with a broken backend
+	ioutil.WriteFile("configJsonA.json", []byte(configJsonE), 0644)
+
+	// test SIGHUP via the kill command
+	// Would not work on windows as kill is not available.
+	// TODO: Implement an alternative test for windows.
+	if runtime.GOOS != "windows" {
+		sigHup()
+		time.Sleep(testPauseDuration) // allow sighup to do its job
+		// did the pidfile change as expected?
+		if _, err := os.Stat("./pidfile2.pid"); os.IsNotExist(err) {
+			t.Error("pidfile not changed after sighup SIGHUP", err)
+		}
+	}
+
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+	//time.Sleep(time.Second * 3)
+	// did backend started as expected?
+	fd, err := os.Open("../../tests/testlog")
+	if err != nil {
+		t.Error(err)
+	}
+	if read, err := ioutil.ReadAll(fd); err == nil {
+		logOutput := string(read)
+		if i := strings.Index(logOutput, "reverted to old backend config"); i < 0 {
+			t.Error("did not revert to old backend config")
+		}
+	}
+
+	// cleanup
+	//os.Truncate("../../tests/testlog", 0)
+	os.Remove("configJsonA.json")
+	os.Remove("./pidfile.pid")
+	os.Remove("./pidfile2.pid")
+
+}

+ 1 - 1
cmd/guerrillad/serve.go

@@ -236,7 +236,7 @@ func writePid(pidFile string) {
 			pid := os.Getpid()
 			pid := os.Getpid()
 			if _, err := f.WriteString(fmt.Sprintf("%d", pid)); err == nil {
 			if _, err := f.WriteString(fmt.Sprintf("%d", pid)); err == nil {
 				f.Sync()
 				f.Sync()
-				mainlog.Infof("pid_file (%s) written with pid:%v", pidFile, pid)
+				mainlog.Infof("(serve.go) pid_file (%s) written with pid:%v", pidFile, pid)
 			} else {
 			} else {
 				mainlog.WithError(err).Fatalf("Error while writing pidFile (%s)", pidFile)
 				mainlog.WithError(err).Fatalf("Error while writing pidFile (%s)", pidFile)
 			}
 			}

+ 6 - 3
cmd/guerrillad/serve_test.go

@@ -45,7 +45,7 @@ var configJsonA = `
             "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
             "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
             "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "timeout":180,
-            "listen_interface":"127.0.0.1:25",
+            "listen_interface":"127.0.0.1:3536",
             "start_tls_on":true,
             "start_tls_on":true,
             "tls_always_on":false,
             "tls_always_on":false,
             "max_clients": 1000,
             "max_clients": 1000,
@@ -94,7 +94,7 @@ var configJsonB = `
             "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
             "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
             "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "timeout":180,
-            "listen_interface":"127.0.0.1:25",
+            "listen_interface":"127.0.0.1:3536",
             "start_tls_on":true,
             "start_tls_on":true,
             "tls_always_on":false,
             "tls_always_on":false,
             "max_clients": 1000,
             "max_clients": 1000,
@@ -364,6 +364,9 @@ func TestCmdConfigChangeEvents(t *testing.T) {
 	for unevent, unfun := range toUnsubscribe {
 	for unevent, unfun := range toUnsubscribe {
 		app.Unsubscribe(unevent, unfun)
 		app.Unsubscribe(unevent, unfun)
 	}
 	}
+	for unevent, unfun := range toUnsubscribeS {
+		app.Unsubscribe(unevent, unfun)
+	}
 
 
 	for event, val := range expectedEvents {
 	for event, val := range expectedEvents {
 		if val == false {
 		if val == false {
@@ -1075,7 +1078,7 @@ func TestSetTimeoutEvent(t *testing.T) {
 // Start in log_level = debug
 // Start in log_level = debug
 // Load config & start server
 // Load config & start server
 func TestDebugLevelChange(t *testing.T) {
 func TestDebugLevelChange(t *testing.T) {
-	//mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	// start the server by emulating the serve command
 	// start the server by emulating the serve command
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)

+ 131 - 12
config.go

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
 	"os"
 	"os"
 	"reflect"
 	"reflect"
 	"strings"
 	"strings"
@@ -13,17 +14,20 @@ import (
 
 
 // AppConfig is the holder of the configuration of the app
 // AppConfig is the holder of the configuration of the app
 type AppConfig struct {
 type AppConfig struct {
-	// Servers can have one or more items. Defaults to 1 server listening to 127.0.0.1:2525
+	// Servers can have one or more items.
+	/// Defaults to 1 server listening on 127.0.0.1:2525
 	Servers []ServerConfig `json:"servers"`
 	Servers []ServerConfig `json:"servers"`
-	// AllowedHosts lists which hosts to accept email for. Defaults to os.Hostname()
+	// AllowedHosts lists which hosts to accept email for. Defaults to os.Hostname
 	AllowedHosts []string `json:"allowed_hosts"`
 	AllowedHosts []string `json:"allowed_hosts"`
 	// PidFile is the path for writing out the process id. No output if empty
 	// PidFile is the path for writing out the process id. No output if empty
 	PidFile string `json:"pid_file"`
 	PidFile string `json:"pid_file"`
-	// LogFile is where the logs go. Use path to file, or "stderr", "stdout" or "off". Default "stderr"
+	// LogFile is where the logs go. Use path to file, or "stderr", "stdout"
+	// or "off". Default "stderr"
 	LogFile string `json:"log_file,omitempty"`
 	LogFile string `json:"log_file,omitempty"`
-	// LogLevel controls the lowest level we log. "info", "debug", "error", "panic". Default "info"
+	// LogLevel controls the lowest level we log.
+	// "info", "debug", "error", "panic". Default "info"
 	LogLevel string `json:"log_level,omitempty"`
 	LogLevel string `json:"log_level,omitempty"`
-	// BackendConfig configures the transaction processing backend
+	// BackendConfig configures the email envelope processing backend
 	BackendConfig backends.BackendConfig `json:"backend_config"`
 	BackendConfig backends.BackendConfig `json:"backend_config"`
 }
 }
 
 
@@ -34,23 +38,27 @@ type ServerConfig struct {
 	// Hostname will be used in the server's reply to HELO/EHLO. If TLS enabled
 	// Hostname will be used in the server's reply to HELO/EHLO. If TLS enabled
 	// make sure that the Hostname matches the cert. Defaults to os.Hostname()
 	// make sure that the Hostname matches the cert. Defaults to os.Hostname()
 	Hostname string `json:"host_name"`
 	Hostname string `json:"host_name"`
-	// MaxSize is the maximum size of an email that will be accepted for delivery. Defaults to 10MB
+	// MaxSize is the maximum size of an email that will be accepted for delivery.
+	// Defaults to 10 Mebibytes
 	MaxSize int64 `json:"max_size"`
 	MaxSize int64 `json:"max_size"`
 	// PrivateKeyFile path to cert private key in PEM format. Will be ignored if blank
 	// PrivateKeyFile path to cert private key in PEM format. Will be ignored if blank
 	PrivateKeyFile string `json:"private_key_file"`
 	PrivateKeyFile string `json:"private_key_file"`
-	// PublicKeyFile path to cert (public key) chain in PEM format. Will be ignored if blank
+	// PublicKeyFile path to cert (public key) chain in PEM format.
+	// Will be ignored if blank
 	PublicKeyFile string `json:"public_key_file"`
 	PublicKeyFile string `json:"public_key_file"`
 	// Timeout specifies the connection timeout in seconds. Defaults to 30
 	// Timeout specifies the connection timeout in seconds. Defaults to 30
 	Timeout int `json:"timeout"`
 	Timeout int `json:"timeout"`
 	// Listen interface specified in <ip>:<port> - defaults to 127.0.0.1:2525
 	// Listen interface specified in <ip>:<port> - defaults to 127.0.0.1:2525
 	ListenInterface string `json:"listen_interface"`
 	ListenInterface string `json:"listen_interface"`
-	// StartTLSOn should we offer STARTTLS command. Cert must be valid. False by default
+	// StartTLSOn should we offer STARTTLS command. Cert must be valid.
+	// False by default
 	StartTLSOn bool `json:"start_tls_on,omitempty"`
 	StartTLSOn bool `json:"start_tls_on,omitempty"`
 	// TLSAlwaysOn run this server as a pure TLS server, i.e. SMTPS
 	// TLSAlwaysOn run this server as a pure TLS server, i.e. SMTPS
 	TLSAlwaysOn bool `json:"tls_always_on,omitempty"`
 	TLSAlwaysOn bool `json:"tls_always_on,omitempty"`
-	// MaxClients controls how many maxiumum clients we can handle at once. Defaults to 100
+	// MaxClients controls how many maxiumum clients we can handle at once.
+	// Defaults to 100
 	MaxClients int `json:"max_clients"`
 	MaxClients int `json:"max_clients"`
-	// LogFile is where the logs go. Use path to file, or "stderr", "stdout" or "off". Default "stderr"
+	// LogFile is where the logs go. Use path to file, or "stderr", "stdout" or "off".
 	// defaults to AppConfig.Log file setting
 	// defaults to AppConfig.Log file setting
 	LogFile string `json:"log_file,omitempty"`
 	LogFile string `json:"log_file,omitempty"`
 
 
@@ -66,8 +74,11 @@ func (c *AppConfig) Load(jsonBytes []byte) error {
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("could not parse config file: %s", err)
 		return fmt.Errorf("could not parse config file: %s", err)
 	}
 	}
-	if len(c.AllowedHosts) == 0 {
-		return errors.New("empty AllowedHosts is not allowed")
+	if err = c.setDefaults(); err != nil {
+		return err
+	}
+	if err = c.setBackendDefaults(); err != nil {
+		return err
 	}
 	}
 
 
 	// all servers must be valid in order to continue
 	// all servers must be valid in order to continue
@@ -151,6 +162,114 @@ func (c *AppConfig) getServers() map[string]*ServerConfig {
 	return servers
 	return servers
 }
 }
 
 
+// setDefaults fills in default server settings for values that were not configured
+// The defaults are:
+// * Server listening to 127.0.0.1:2525
+// * use your hostname to determine your which hosts to accept email for
+// * 100 maximum clients
+// * 10MB max message size
+// * log to Stderr,
+// * log level set to "`debug`"
+// * timeout to 30 sec
+// * Backend configured with the following processors: `HeadersParser|Header|Debugger`
+// where it will log the received emails.
+func (c *AppConfig) setDefaults() error {
+	if c.LogFile == "" {
+		c.LogFile = log.OutputStderr.String()
+	}
+	if c.LogLevel == "" {
+		c.LogLevel = "debug"
+	}
+	if len(c.AllowedHosts) == 0 {
+		if h, err := os.Hostname(); err != nil {
+			return err
+		} else {
+			c.AllowedHosts = append(c.AllowedHosts, h)
+		}
+	}
+	h, err := os.Hostname()
+	if err != nil {
+		return err
+	}
+	if len(c.Servers) == 0 {
+		sc := ServerConfig{}
+		sc.LogFile = c.LogFile
+		sc.ListenInterface = defaultInterface
+		sc.IsEnabled = true
+		sc.Hostname = h
+		sc.MaxClients = 100
+		sc.Timeout = 30
+		sc.MaxSize = 10 << 20 // 10 Mebibytes
+		c.Servers = append(c.Servers, sc)
+	} else {
+		// make sure each server has defaults correctly configured
+		for i := range c.Servers {
+			if c.Servers[i].Hostname == "" {
+				c.Servers[i].Hostname = h
+			}
+			if c.Servers[i].MaxClients == 0 {
+				c.Servers[i].MaxClients = 100
+			}
+			if c.Servers[i].Timeout == 0 {
+				c.Servers[i].Timeout = 20
+			}
+			if c.Servers[i].MaxSize == 0 {
+				c.Servers[i].MaxSize = 10 << 20 // 10 Mebibytes
+			}
+			if c.Servers[i].ListenInterface == "" {
+				return errors.New(fmt.Sprintf("Listen interface not specified for server at index %d", i))
+			}
+			if c.Servers[i].LogFile == "" {
+				c.Servers[i].LogFile = c.LogFile
+			}
+			// validate the server config
+			err = c.Servers[i].Validate()
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+// setBackendDefaults sets default values for the backend config,
+// if no backend config was added before starting, then use a default config
+// otherwise, see what required values were missed in the config and add any missing with defaults
+func (c *AppConfig) setBackendDefaults() error {
+
+	if len(c.BackendConfig) == 0 {
+		h, err := os.Hostname()
+		if err != nil {
+			return err
+		}
+		c.BackendConfig = backends.BackendConfig{
+			"log_received_mails": true,
+			"save_workers_size":  1,
+			"process_stack":      "HeadersParser|Header|Debugger",
+			"primary_mail_host":  h,
+		}
+	} else {
+		if _, ok := c.BackendConfig["process_stack"]; !ok {
+			c.BackendConfig["process_stack"] = "HeadersParser|Header|Debugger"
+		}
+		if _, ok := c.BackendConfig["primary_mail_host"]; !ok {
+			h, err := os.Hostname()
+			if err != nil {
+				return err
+			}
+			c.BackendConfig["primary_mail_host"] = h
+		}
+		if _, ok := c.BackendConfig["save_workers_size"]; !ok {
+			c.BackendConfig["save_workers_size"] = 1
+		}
+
+		if _, ok := c.BackendConfig["log_received_mails"]; !ok {
+			c.BackendConfig["log_received_mails"] = false
+		}
+	}
+	return nil
+}
+
 // Emits any configuration change events on the server.
 // Emits any configuration change events on the server.
 // All events are fired and run synchronously
 // All events are fired and run synchronously
 func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla) {
 func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla) {

+ 13 - 5
config_test.go

@@ -22,7 +22,7 @@ var configJsonA = `
 {
 {
     "log_file" : "./tests/testlog",
     "log_file" : "./tests/testlog",
     "log_level" : "debug",
     "log_level" : "debug",
-    "pid_file" : "/var/run/go-guerrilla.pid",
+    "pid_file" : "tests/go-guerrilla.pid",
     "allowed_hosts": ["spam4.me","grr.la"],
     "allowed_hosts": ["spam4.me","grr.la"],
     "backend_config" :
     "backend_config" :
         {
         {
@@ -95,7 +95,7 @@ var configJsonB = `
 {
 {
     "log_file" : "./tests/testlog",
     "log_file" : "./tests/testlog",
     "log_level" : "debug",
     "log_level" : "debug",
-    "pid_file" : "/var/run/different-go-guerrilla.pid",
+    "pid_file" : "tests/different-go-guerrilla.pid",
     "allowed_hosts": ["spam4.me","grr.la","newhost.com"],
     "allowed_hosts": ["spam4.me","grr.la","newhost.com"],
     "backend_config" :
     "backend_config" :
         {
         {
@@ -124,6 +124,7 @@ var configJsonB = `
             "listen_interface":"127.0.0.1:2527",
             "listen_interface":"127.0.0.1:2527",
             "start_tls_on":true,
             "start_tls_on":true,
             "tls_always_on":false,
             "tls_always_on":false,
+            "log_file" : "./tests/testlog",
             "max_clients": 2
             "max_clients": 2
         },
         },
 
 
@@ -136,7 +137,7 @@ var configJsonB = `
             "timeout":180,
             "timeout":180,
             "listen_interface":"127.0.0.1:4654",
             "listen_interface":"127.0.0.1:4654",
             "start_tls_on":false,
             "start_tls_on":false,
-            "tls_always_on":true,
+            "tls_always_on":false,
             "max_clients":1
             "max_clients":1
         },
         },
 
 
@@ -197,9 +198,16 @@ func TestConfigChangeEvents(t *testing.T) {
 	oldconf.Load([]byte(configJsonA))
 	oldconf.Load([]byte(configJsonA))
 	logger, _ := log.GetLogger(oldconf.LogFile)
 	logger, _ := log.GetLogger(oldconf.LogFile)
 	bcfg := backends.BackendConfig{"log_received_mails": true}
 	bcfg := backends.BackendConfig{"log_received_mails": true}
-	backend, _ := backends.New(bcfg, logger)
-	app, _ := New(oldconf, backend, logger)
+	backend, err := backends.New(bcfg, logger)
+	if err != nil {
+		t.Error("cannot create backend", err)
+	}
+	app, err := New(oldconf, backend, logger)
+	if err != nil {
+		t.Error("cannot create daemon", err)
+	}
 	// simulate timestamp change
 	// simulate timestamp change
+
 	time.Sleep(time.Second + time.Millisecond*500)
 	time.Sleep(time.Second + time.Millisecond*500)
 	os.Chtimes(oldconf.Servers[1].PrivateKeyFile, time.Now(), time.Now())
 	os.Chtimes(oldconf.Servers[1].PrivateKeyFile, time.Now(), time.Now())
 	os.Chtimes(oldconf.Servers[1].PublicKeyFile, time.Now(), time.Now())
 	os.Chtimes(oldconf.Servers[1].PublicKeyFile, time.Now(), time.Now())

+ 78 - 20
guerrilla.go

@@ -2,8 +2,10 @@ package guerrilla
 
 
 import (
 import (
 	"errors"
 	"errors"
+	"fmt"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/log"
+	"os"
 	"sync"
 	"sync"
 	"sync/atomic"
 	"sync/atomic"
 )
 )
@@ -44,18 +46,22 @@ type Guerrilla interface {
 type guerrilla struct {
 type guerrilla struct {
 	Config  AppConfig
 	Config  AppConfig
 	servers map[string]*server
 	servers map[string]*server
-	backend backends.Backend
 	// guard controls access to g.servers
 	// guard controls access to g.servers
 	guard sync.Mutex
 	guard sync.Mutex
 	state int8
 	state int8
 	EventHandler
 	EventHandler
 	logStore
 	logStore
+	backendStore
 }
 }
 
 
 type logStore struct {
 type logStore struct {
 	atomic.Value
 	atomic.Value
 }
 }
 
 
+type backendStore struct {
+	atomic.Value
+}
+
 // Get loads the log.logger in an atomic operation. Returns a stderr logger if not able to load
 // Get loads the log.logger in an atomic operation. Returns a stderr logger if not able to load
 func (ls *logStore) mainlog() log.Logger {
 func (ls *logStore) mainlog() log.Logger {
 	if v, ok := ls.Load().(log.Logger); ok {
 	if v, ok := ls.Load().(log.Logger); ok {
@@ -75,8 +81,8 @@ func New(ac *AppConfig, b backends.Backend, l log.Logger) (Guerrilla, error) {
 	g := &guerrilla{
 	g := &guerrilla{
 		Config:  *ac, // take a local copy
 		Config:  *ac, // take a local copy
 		servers: make(map[string]*server, len(ac.Servers)),
 		servers: make(map[string]*server, len(ac.Servers)),
-		backend: b,
 	}
 	}
+	g.backendStore.Store(b)
 	g.setMainlog(l)
 	g.setMainlog(l)
 
 
 	if ac.LogLevel != "" {
 	if ac.LogLevel != "" {
@@ -87,19 +93,24 @@ func New(ac *AppConfig, b backends.Backend, l log.Logger) (Guerrilla, error) {
 	err := g.makeServers()
 	err := g.makeServers()
 
 
 	// start backend for processing email
 	// start backend for processing email
-	err = g.backend.Start()
+	err = g.backend().Start()
+
 	if err != nil {
 	if err != nil {
 		return g, err
 		return g, err
 	}
 	}
+	g.writePid()
 
 
 	// subscribe for any events that may come in while running
 	// subscribe for any events that may come in while running
 	g.subscribeEvents()
 	g.subscribeEvents()
+
 	return g, err
 	return g, err
 }
 }
 
 
 // Instantiate servers
 // Instantiate servers
 func (g *guerrilla) makeServers() error {
 func (g *guerrilla) makeServers() error {
 	g.mainlog().Debug("making servers")
 	g.mainlog().Debug("making servers")
+	g.guard.Lock()
+	defer g.guard.Unlock()
 	var errs Errors
 	var errs Errors
 	for _, sc := range g.Config.Servers {
 	for _, sc := range g.Config.Servers {
 		if _, ok := g.servers[sc.ListenInterface]; ok {
 		if _, ok := g.servers[sc.ListenInterface]; ok {
@@ -111,7 +122,7 @@ func (g *guerrilla) makeServers() error {
 			errs = append(errs, err)
 			errs = append(errs, err)
 			continue
 			continue
 		} else {
 		} else {
-			server, err := newServer(&sc, g.backend, g.mainlog())
+			server, err := newServer(&sc, g.backend(), g.mainlog())
 			if err != nil {
 			if err != nil {
 				g.mainlog().WithError(err).Errorf("Failed to create server [%s]", sc.ListenInterface)
 				g.mainlog().WithError(err).Errorf("Failed to create server [%s]", sc.ListenInterface)
 				errs = append(errs, err)
 				errs = append(errs, err)
@@ -131,7 +142,7 @@ func (g *guerrilla) makeServers() error {
 	return errs
 	return errs
 }
 }
 
 
-// find a server by interface, retuning the server or err
+// findServer finds a server by iface (interface), retuning the server or err
 func (g *guerrilla) findServer(iface string) (*server, error) {
 func (g *guerrilla) findServer(iface string) (*server, error) {
 	g.guard.Lock()
 	g.guard.Lock()
 	defer g.guard.Unlock()
 	defer g.guard.Unlock()
@@ -141,6 +152,7 @@ func (g *guerrilla) findServer(iface string) (*server, error) {
 	return nil, errors.New("server not found in g.servers")
 	return nil, errors.New("server not found in g.servers")
 }
 }
 
 
+// removeServer removes a server from the list of servers
 func (g *guerrilla) removeServer(iface string) {
 func (g *guerrilla) removeServer(iface string) {
 	g.guard.Lock()
 	g.guard.Lock()
 	defer g.guard.Unlock()
 	defer g.guard.Unlock()
@@ -222,6 +234,11 @@ func (g *guerrilla) subscribeEvents() {
 		g.mainlog().Infof("log level changed to [%s]", c.LogLevel)
 		g.mainlog().Infof("log level changed to [%s]", c.LogLevel)
 	})
 	})
 
 
+	// write out our pid whenever the file name changes in the config
+	g.Subscribe(EventConfigPidFile, func(ac *AppConfig) {
+		g.writePid()
+	})
+
 	// server config was updated
 	// server config was updated
 	g.Subscribe(EventConfigServerConfig, func(sc *ServerConfig) {
 	g.Subscribe(EventConfigServerConfig, func(sc *ServerConfig) {
 		g.setServerConfig(sc)
 		g.setServerConfig(sc)
@@ -229,8 +246,10 @@ func (g *guerrilla) subscribeEvents() {
 
 
 	// add a new server to the config & start
 	// add a new server to the config & start
 	g.Subscribe(EventConfigServerNew, func(sc *ServerConfig) {
 	g.Subscribe(EventConfigServerNew, func(sc *ServerConfig) {
+		g.mainlog().Debugf("event fired [%s] %s", EventConfigServerNew, sc.ListenInterface)
 		if _, err := g.findServer(sc.ListenInterface); err != nil {
 		if _, err := g.findServer(sc.ListenInterface); err != nil {
 			// not found, lets add it
 			// not found, lets add it
+			//
 			if err := g.makeServers(); err != nil {
 			if err := g.makeServers(); err != nil {
 				g.mainlog().WithError(err).Errorf("cannot add server [%s]", sc.ListenInterface)
 				g.mainlog().WithError(err).Errorf("cannot add server [%s]", sc.ListenInterface)
 				return
 				return
@@ -242,6 +261,8 @@ func (g *guerrilla) subscribeEvents() {
 					g.mainlog().WithError(err).Info("Event server_change:new_server returned errors when starting")
 					g.mainlog().WithError(err).Info("Event server_change:new_server returned errors when starting")
 				}
 				}
 			}
 			}
+		} else {
+			g.mainlog().Debugf("new event, but server already fund")
 		}
 		}
 	})
 	})
 	// start a server that already exists in the config and has been enabled
 	// start a server that already exists in the config and has been enabled
@@ -306,7 +327,6 @@ func (g *guerrilla) subscribeEvents() {
 				backends.Svc.SetMainlog(l)
 				backends.Svc.SetMainlog(l)
 				// it will change to the new logger on the next accepted client
 				// it will change to the new logger on the next accepted client
 				server.logStore.Store(l)
 				server.logStore.Store(l)
-
 				g.mainlog().Infof("Server [%s] changed, new clients will log to: [%s]",
 				g.mainlog().Infof("Server [%s] changed, new clients will log to: [%s]",
 					sc.ListenInterface,
 					sc.ListenInterface,
 					sc.LogFile,
 					sc.LogFile,
@@ -332,19 +352,19 @@ func (g *guerrilla) subscribeEvents() {
 		logger, _ := log.GetLogger(appConfig.LogFile)
 		logger, _ := log.GetLogger(appConfig.LogFile)
 		// shutdown the backend first.
 		// shutdown the backend first.
 		var err error
 		var err error
-		if err = g.backend.Shutdown(); err != nil {
+		if err = g.backend().Shutdown(); err != nil {
 			logger.WithError(err).Warn("Backend failed to shutdown")
 			logger.WithError(err).Warn("Backend failed to shutdown")
 			return
 			return
 		}
 		}
-		// init a new backend, Revert to old backend config if it failes
+		// init a new backend, Revert to old backend config if it fails
 		if newBackend, newErr := backends.New(appConfig.BackendConfig, logger); newErr != nil {
 		if newBackend, newErr := backends.New(appConfig.BackendConfig, logger); newErr != nil {
 			logger.WithError(newErr).Error("Error while loading the backend")
 			logger.WithError(newErr).Error("Error while loading the backend")
-			err = g.backend.Reinitialize()
+			err = g.backend().Reinitialize()
 			if err != nil {
 			if err != nil {
 				logger.WithError(err).Fatal("failed to revert to old backend config")
 				logger.WithError(err).Fatal("failed to revert to old backend config")
 				return
 				return
 			}
 			}
-			err = g.backend.Start()
+			err = g.backend().Start()
 			if err != nil {
 			if err != nil {
 				logger.WithError(err).Fatal("failed to start backend with old config")
 				logger.WithError(err).Fatal("failed to start backend with old config")
 				return
 				return
@@ -352,14 +372,29 @@ func (g *guerrilla) subscribeEvents() {
 			logger.Info("reverted to old backend config")
 			logger.Info("reverted to old backend config")
 		} else {
 		} else {
 			// swap to the bew backend (assuming old backend was shutdown so it can be safely swapped)
 			// swap to the bew backend (assuming old backend was shutdown so it can be safely swapped)
-			newBackend.Start()
-			g.backend = newBackend
-			logger.Info("new backend started")
+			if err := newBackend.Start(); err != nil {
+				logger.WithError(err).Error("backend could not start")
+			}
+			g.storeBackend(newBackend)
 		}
 		}
 	})
 	})
 
 
 }
 }
 
 
+func (g *guerrilla) storeBackend(b backends.Backend) {
+	g.backendStore.Store(b)
+	g.mapServers(func(server *server) {
+		server.setBackend(b)
+	})
+}
+
+func (g *guerrilla) backend() backends.Backend {
+	if b, ok := g.backendStore.Load().(backends.Backend); ok {
+		return b
+	}
+	return nil
+}
+
 // Entry point for the application. Starts all servers.
 // Entry point for the application. Starts all servers.
 func (g *guerrilla) Start() error {
 func (g *guerrilla) Start() error {
 	var startErrors Errors
 	var startErrors Errors
@@ -373,8 +408,8 @@ func (g *guerrilla) Start() error {
 	}
 	}
 	if g.state == GuerrillaStateStopped {
 	if g.state == GuerrillaStateStopped {
 		// when a backend is shutdown, we need to re-initialize before it can be started again
 		// when a backend is shutdown, we need to re-initialize before it can be started again
-		g.backend.Reinitialize()
-		g.backend.Start()
+		g.backend().Reinitialize()
+		g.backend().Start()
 	}
 	}
 	// channel for reading errors
 	// channel for reading errors
 	errs := make(chan error, len(g.servers))
 	errs := make(chan error, len(g.servers))
@@ -382,7 +417,6 @@ func (g *guerrilla) Start() error {
 
 
 	// start servers, send any errors back to errs channel
 	// start servers, send any errors back to errs channel
 	for ListenInterface := range g.servers {
 	for ListenInterface := range g.servers {
-		g.mainlog().Infof("Starting: %s", ListenInterface)
 		if !g.servers[ListenInterface].isEnabled() {
 		if !g.servers[ListenInterface].isEnabled() {
 			// not enabled
 			// not enabled
 			continue
 			continue
@@ -393,6 +427,7 @@ func (g *guerrilla) Start() error {
 		}
 		}
 		startWG.Add(1)
 		startWG.Add(1)
 		go func(s *server) {
 		go func(s *server) {
+			g.mainlog().Infof("Starting: %s", s.listenInterface)
 			if err := s.Start(&startWG); err != nil {
 			if err := s.Start(&startWG); err != nil {
 				errs <- err
 				errs <- err
 			}
 			}
@@ -420,13 +455,14 @@ func (g *guerrilla) Shutdown() {
 		g.state = GuerrillaStateStopped
 		g.state = GuerrillaStateStopped
 		defer g.guard.Unlock()
 		defer g.guard.Unlock()
 	}()
 	}()
-	for ListenInterface, s := range g.servers {
+	g.mapServers(func(s *server) {
 		if s.state == ServerStateRunning {
 		if s.state == ServerStateRunning {
 			s.Shutdown()
 			s.Shutdown()
-			g.mainlog().Infof("shutdown completed for [%s]", ListenInterface)
+			g.mainlog().Infof("shutdown completed for [%s]", s.listenInterface)
 		}
 		}
-	}
-	if err := g.backend.Shutdown(); err != nil {
+	})
+
+	if err := g.backend().Shutdown(); err != nil {
 		g.mainlog().WithError(err).Warn("Backend failed to shutdown")
 		g.mainlog().WithError(err).Warn("Backend failed to shutdown")
 	} else {
 	} else {
 		g.mainlog().Infof("Backend shutdown completed")
 		g.mainlog().Infof("Backend shutdown completed")
@@ -439,3 +475,25 @@ func (g *guerrilla) SetLogger(l log.Logger) {
 	g.setMainlog(l)
 	g.setMainlog(l)
 	backends.Svc.SetMainlog(l)
 	backends.Svc.SetMainlog(l)
 }
 }
+
+// writePid writes the pid (process id) to the file specified in the config.
+// Won't write anything if no file specified
+func (g *guerrilla) writePid() error {
+	if len(g.Config.PidFile) > 0 {
+		if f, err := os.Create(g.Config.PidFile); err == nil {
+			defer f.Close()
+			pid := os.Getpid()
+			if _, err := f.WriteString(fmt.Sprintf("%d", pid)); err == nil {
+				f.Sync()
+				g.mainlog().Infof("pid_file (%s) written with pid:%v", g.Config.PidFile, pid)
+			} else {
+				g.mainlog().WithError(err).Errorf("Error while writing pidFile (%s)", g.Config.PidFile)
+				return err
+			}
+		} else {
+			g.mainlog().WithError(err).Errorf("Error while creating pidFile (%s)", g.Config.PidFile)
+			return err
+		}
+	}
+	return nil
+}

+ 18 - 4
server.go

@@ -46,7 +46,6 @@ const (
 // Server listens for SMTP clients on the port specified in its config
 // Server listens for SMTP clients on the port specified in its config
 type server struct {
 type server struct {
 	configStore     atomic.Value // stores guerrilla.ServerConfig
 	configStore     atomic.Value // stores guerrilla.ServerConfig
-	backend         backends.Backend
 	tlsConfigStore  atomic.Value
 	tlsConfigStore  atomic.Value
 	timeout         atomic.Value // stores time.Duration
 	timeout         atomic.Value // stores time.Duration
 	listenInterface string
 	listenInterface string
@@ -61,6 +60,7 @@ type server struct {
 	// If log changed after a config reload, newLogStore stores the value here until it's safe to change it
 	// If log changed after a config reload, newLogStore stores the value here until it's safe to change it
 	logStore     atomic.Value
 	logStore     atomic.Value
 	mainlogStore atomic.Value
 	mainlogStore atomic.Value
+	backendStore atomic.Value
 }
 }
 
 
 type allowedHosts struct {
 type allowedHosts struct {
@@ -71,13 +71,13 @@ type allowedHosts struct {
 // Creates and returns a new ready-to-run Server from a configuration
 // Creates and returns a new ready-to-run Server from a configuration
 func newServer(sc *ServerConfig, b backends.Backend, l log.Logger) (*server, error) {
 func newServer(sc *ServerConfig, b backends.Backend, l log.Logger) (*server, error) {
 	server := &server{
 	server := &server{
-		backend:         b,
 		clientPool:      NewPool(sc.MaxClients),
 		clientPool:      NewPool(sc.MaxClients),
 		closedListener:  make(chan (bool), 1),
 		closedListener:  make(chan (bool), 1),
 		listenInterface: sc.ListenInterface,
 		listenInterface: sc.ListenInterface,
 		state:           ServerStateNew,
 		state:           ServerStateNew,
 		mainlog:         l,
 		mainlog:         l,
 	}
 	}
+	server.backendStore.Store(b)
 	var logOpenError error
 	var logOpenError error
 	if sc.LogFile == "" {
 	if sc.LogFile == "" {
 		// none set, use the same log file as mainlog
 		// none set, use the same log file as mainlog
@@ -135,6 +135,19 @@ func (s *server) configureLog() {
 	}
 	}
 }
 }
 
 
+// setBackend Sets the backend to use for processing email envelopes
+func (s *server) setBackend(b backends.Backend) {
+	s.backendStore.Store(b)
+}
+
+// backend gets the backend used to process email envelopes
+func (s *server) backend() backends.Backend {
+	if b, ok := s.backendStore.Load().(backends.Backend); ok {
+		return b
+	}
+	return nil
+}
+
 // Set the timeout for the server and all clients
 // Set the timeout for the server and all clients
 func (server *server) setTimeout(seconds int) {
 func (server *server) setTimeout(seconds int) {
 	duration := time.Duration(int64(seconds))
 	duration := time.Duration(int64(seconds))
@@ -410,7 +423,8 @@ func (server *server) handleClient(client *client) {
 						client.sendResponse(response.Canned.ErrorRelayDenied, to.Host)
 						client.sendResponse(response.Canned.ErrorRelayDenied, to.Host)
 					} else {
 					} else {
 						client.PushRcpt(to)
 						client.PushRcpt(to)
-						rcptError := server.backend.ValidateRcpt(client.Envelope)
+						server.log.Info("Server backend is: ", server.backend)
+						rcptError := server.backend().ValidateRcpt(client.Envelope)
 						if rcptError != nil {
 						if rcptError != nil {
 							client.PopRcpt()
 							client.PopRcpt()
 							client.sendResponse(response.Canned.FailRcptCmd + " " + rcptError.Error())
 							client.sendResponse(response.Canned.FailRcptCmd + " " + rcptError.Error())
@@ -487,7 +501,7 @@ func (server *server) handleClient(client *client) {
 				break
 				break
 			}
 			}
 
 
-			res := server.backend.Process(client.Envelope)
+			res := server.backend().Process(client.Envelope)
 			if res.Code() < 300 {
 			if res.Code() < 300 {
 				client.messagesSent++
 				client.messagesSent++
 			}
 			}

+ 1 - 1
tests/guerrilla_test.go

@@ -74,7 +74,7 @@ var configJson = `
 {
 {
     "log_file" : "./testlog",
     "log_file" : "./testlog",
     "log_level" : "debug",
     "log_level" : "debug",
-    "pid_file" : "/var/run/go-guerrilla.pid",
+    "pid_file" : "go-guerrilla.pid",
     "allowed_hosts": ["spam4.me","grr.la"],
     "allowed_hosts": ["spam4.me","grr.la"],
     "backend_config" :
     "backend_config" :
         {
         {