* Added capability to respawn as background process.
authorUrban Wallasch <urban.wallasch@freenet.de>
Thu, 11 Apr 2019 13:44:21 +0000 (15:44 +0200)
committerUrban Wallasch <urban.wallasch@freenet.de>
Thu, 11 Apr 2019 13:44:21 +0000 (15:44 +0200)
* Made some (partially related) changes to logging.
* Updated README.md.

README.md
config.go
gogopherd.cfg.example
gogopherd.go
logger.go

index c078f28bf6459e717bcc42e56322d834d089e137..bcfe73dfbc9bc74726293924f86697ee97eed03f 100644 (file)
--- a/README.md
+++ b/README.md
@@ -31,27 +31,30 @@ packages are referenced. A Makefile is provided to regenerate the
 
 ## Usage
 
-The `gogopherd` executable recognizes the following command line
-options:
+`Gogopherd` recognizes the following command line options:
 
 ```
+
   -D
       allow generated directory indexes
 
-  -F string
-      footer file to append to generated pages
+  -F file
+      footer: append contents of file to generated pages
 
-  -H string
-      header file to prepend to generated pages
+  -H file
+      header: prepend contents of file to generated pages
 
-  -I string
-      directory index page (default "index.goph")
+  -I file
+      directory index file name (default "index.goph")
 
-  -L string
+  -L file
       write log to file; empty for stderr
 
-  -c string
-      configuration file
+  -b user
+      restart the process in the background as user
+
+  -c file
+      read configuration from file
 
   -d
       allow access to dotfiles
@@ -60,37 +63,38 @@ options:
       fully qualified domain name (default "localhost")
 
   -h
-      show this help page
+      display this help message
 
   -i string
       interface to bind to; empty for any (default "localhost")
 
-  -lb int
-      set connection burst limit (default 50)
+  -lb num
+      set connection burst limit to num (default 50)
 
-  -lc int
-      set connections per second rate limit  (default 5)
+  -lc num
+      set connections per second rate limit to num (default 5)
 
-  -lj int
-      set worker job queue size (default 50)
+  -lj num
+      set worker job queue size to num (default 50)
 
-  -lw int
-      set number of worker threads (default 6)
+  -lw num
+      set number of worker threads to num (default 6)
 
-  -p string
+  -p port
       TCP port to listen on (default "7070")
 
-  -r string
+  -r directory
       document root directory (default ".")
 
   -s
       follow symbolic links
 
-  -t int
+  -t seconds
       connection read/write timeout in seconds (default 60)
 
   -v
       produce verbose output
+
 ```
 
 A commented example configuration file, `gogopherd.cfg.example`, is
@@ -102,7 +106,7 @@ line will override the respective settings in the configuration file.
 
 A small sample gopherhole is included in this project. You can test it
 by simply copying `gogopherd.cfg.example` to `gogopherd.cfg` and
-starting gogopherd. Now point your gopher client to localhost port 70
+starting gogopherd. Now point your gopher client to localhost port 7070
 and you are good to go.
 
 
index 6e5ea1dbece4055e8edb600fb0ddea2212f75348..0403cb02aa6c20452057348fe540bbf30a65260a 100644 (file)
--- a/config.go
+++ b/config.go
@@ -37,6 +37,7 @@ var cfg = struct {
        njobs   int
        nworker int
        logfile string
+       bkguser string
        verbose bool
 }{
        iface:   "localhost",
@@ -55,6 +56,7 @@ var cfg = struct {
        nworker: 6,
        njobs:   50,
        logfile: "",
+       bkguser: "",
        verbose: false,
 }
 
@@ -107,6 +109,8 @@ func parseConfigFile(filename string) error {
                        cfg.njobs, _ = strconv.Atoi(val)
                case "logfile":
                        cfg.logfile = val
+               case "bkguser":
+                       cfg.bkguser = val
                case "verbose":
                        cfg.verbose = strToBool(val)
                default:
@@ -138,6 +142,7 @@ func initialize() {
        flag.IntVar(&cfg.nworker, "lw", cfg.nworker, "set number of worker threads to `num`")
        flag.IntVar(&cfg.njobs, "lj", cfg.njobs, "set worker job queue size to `num`")
        flag.StringVar(&cfg.logfile, "L", cfg.logfile, "write log to `file`; empty for stderr")
+       flag.StringVar(&cfg.bkguser, "b", cfg.bkguser, "restart the process in the background as `user`")
        flag.BoolVar(&cfg.verbose, "v", cfg.verbose, "produce verbose output")
        flag.Parse()
        if help {
@@ -187,6 +192,8 @@ func initialize() {
        tracer.Print("cburst:  ", cfg.cburst)
        tracer.Print("nworker: ", cfg.nworker)
        tracer.Print("njobs:   ", cfg.njobs)
+       tracer.Print("logfile: ", cfg.logfile)
+       tracer.Print("bkguser: ", cfg.bkguser)
        tracer.Print("verbose: ", cfg.verbose)
        tracer.Print("gogopherd version ", version)
 }
index 8dfa9a8b1654c25b95f04893c831eb0b45a6359c..14809aa0fc6c053eb70c5c4e2c8b29b101a26ed1 100644 (file)
@@ -38,6 +38,9 @@ njobs = 50
 # Log to file; leave empty for stderr
 logfile = ""
 
+# Daemonize as this user; leave empty for foreground operation
+bkguser = ""
+
 # Produce verbose output
 verbose = false
 
index 8b8b577cc13561f422e192c0c506a39deebea788..30773e3328a6ee73897669eec14366f132d5a98d 100644 (file)
@@ -14,13 +14,17 @@ package main
 
 import (
        "bufio"
+       "fmt"
        "io"
        "io/ioutil"
        "net"
        "os"
+       "os/exec"
        "os/signal"
+       "os/user"
        "path"
        "path/filepath"
+       "strconv"
        "strings"
        "syscall"
        "time"
@@ -174,7 +178,7 @@ func handleRequest(conn net.Conn) {
                return
        }
        req = strings.TrimSpace(req)
-       tracer.Print("request: '", req, "'")
+       logger.Print("[", conn.RemoteAddr(),"] '", req, "'")
        // canonicalize and validate referenced path
        rpath, err := validatePath(cfg.docroot, filepath.Join(cfg.docroot, filepath.FromSlash(req)))
        if check(err, "validatePath "+rpath) != nil {
@@ -247,7 +251,7 @@ func handleRequest(conn net.Conn) {
        return
 }
 
-func serveTCP(listener *net.TCPListener) {
+func serveTCP(listener net.Listener) {
        // connection rate limiter
        tick := time.NewTicker(time.Second / time.Duration(cfg.crate))
        defer tick.Stop()
@@ -274,28 +278,117 @@ func serveTCP(listener *net.TCPListener) {
        defer listener.Close()
        logger.Print("listening on TCP ", listener.Addr())
        for {
-               conn, err := listener.AcceptTCP()
+               conn, err := listener.Accept()
                if shutting_down {
                        return
                }
                if checkFatal(err, "Accept") == nil {
-                       logger.Print("connect from ", conn.RemoteAddr())
+                       logger.Print("[", conn.RemoteAddr(),"] connect")
                        jobs <- conn
                }
                <-throttle
        }
 }
 
+func daemonize(asuser string, stdfiles []*os.File, xfiles []*os.File, env []string) (int, error) {
+       userCurrent, err := user.Current()
+       if err != nil {
+               return -1, err
+       }
+       userRequired, err := user.Lookup(asuser)
+       if err != nil {
+               return -1, err
+       }
+       executable, err := filepath.Abs(os.Args[0])
+       if err != nil {
+               return -1, err
+       }
+       cmd := exec.Command(executable)
+       cmd.Args = append([]string{executable}, os.Args[1:]...)
+       cmd.Env = append(env, []string{
+               fmt.Sprintf("USER=%s", userRequired.Username),
+               fmt.Sprintf("HOME=%s", userRequired.HomeDir),
+               fmt.Sprintf("NUMXFD=%d", len(xfiles)),
+       }...)
+       if userCurrent.Uid != userRequired.Uid {
+               uid, _ := strconv.Atoi(userRequired.Uid)
+               gid, _ := strconv.Atoi(userRequired.Gid)
+               cmd.SysProcAttr = &syscall.SysProcAttr{
+                       Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)},
+               }
+       }
+       if len(stdfiles) > 0 {
+               cmd.Stdin = stdfiles[0]
+       }
+       if len(stdfiles) > 1 {
+               cmd.Stdout = stdfiles[1]
+       }
+       if len(stdfiles) > 2 {
+               cmd.Stderr = stdfiles[2]
+       }
+       cmd.ExtraFiles = xfiles
+       err = cmd.Start()
+       if err != nil {
+               return -1, err
+       }
+       return cmd.Process.Pid, nil
+}
+
 func main() {
        initialize()
 
-       // listen on interface:port
-       service := cfg.iface + ":" + cfg.port
-       address, err := net.ResolveTCPAddr("tcp", service)
-       checkFatal(err, "ResolveTCPAddr")
-       listener, err := net.ListenTCP("tcp", address)
-       checkFatal(err, "ListenTCP")
-       go serveTCP(listener)
+       if cfg.bkguser == "" {
+               // run as foreground process
+               address := cfg.iface + ":" + cfg.port
+               listener, err := net.Listen("tcp", address)
+               checkFatal(err, "Listen")
+               go serveTCP(listener)
+       } else {
+               const isdaemon = "_ISDAEMON_"
+               if os.Getenv(isdaemon) != "1" {
+                       // parent
+                       service := cfg.iface + ":" + cfg.port
+                       address, err := net.ResolveTCPAddr("tcp", service)
+                       checkFatal(err, "ResolveTCPAddr")
+                       listener, err := net.ListenTCP("tcp", address)
+                       checkFatal(err, "ListenTCP")
+                       lf, err := listener.File()
+                       checkFatal(err, "listener.File")
+                       fxtra := []*os.File{
+                               lf,
+                       }
+                       fstd := []*os.File{
+                               nil,
+                               nil,
+                               os.Stderr,  // inherit our logging fd
+                       }
+                       env := []string{
+                               "PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"",
+                               fmt.Sprintf("%s=1", isdaemon),
+                       }
+                       cpid, err := daemonize(cfg.bkguser, fstd, fxtra, env)
+                       checkFatal(err, "daemonize")
+                       tracer.Print("background process spawned, child pid=", cpid, ", bye.")
+                       os.Exit(0)
+               } else {
+                       // child
+                       pid := os.Getpid()
+                       ppid := os.Getppid()
+                       user, _ := user.Current()
+                       tracer.Print("daemon pid=", pid, ", ppid=", ppid, ", user=", user)
+                       tracer.Print("daemon args=", os.Args)
+                       tracer.Print("daemon env=", os.Environ())
+                       nxfd, err := strconv.Atoi(os.Getenv("NUMXFD"))
+                       check(err, "Atoi(Getenv())")
+                       if nxfd < 1 {
+                               fmt.Println("too few extra files", err)
+                               os.Exit(1)
+                       }
+                       listener, err := net.FileListener(os.NewFile(3, "listener"))
+                       checkFatal(err, "FileListener")
+                       go serveTCP(listener)
+               }
+       }
 
        // wait for signal
        sigchan := make(chan os.Signal)
index c5a896b2cdca07882e8c008e0ed4db86849517f5..00882071276cb0538f06951de15544b85b1c442b 100644 (file)
--- a/logger.go
+++ b/logger.go
@@ -28,10 +28,11 @@ func initLogger(verbose bool, logfilename string) {
        if len(logfilename) > 0 {
                logfile, err := os.OpenFile(logfilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
                if err != nil {
-                       log.Fatalln("Failed to open log file", logfilename, ":", err)
+                       log.Print("Failed to open log file", logfilename, ":", err)
+               } else {
+                       os.Stderr.Close()
+                       os.Stderr = logfile
                }
-               os.Stderr.Close()
-               os.Stderr = logfile
        }
        logger = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lshortfile)
        loggex = log.New(os.Stderr, "", log.Ldate|log.Ltime)