diff --git a/Readme.md b/Readme.md index 5048738..3902aad 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,7 @@ # ownCloud Infinite Scale: Runtime [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8badecde63f743868c71850e43cdeb0d)](https://app.codacy.com/manual/refs_2/pman?utm_source=github.com&utm_medium=referral&utm_content=refs/pman&utm_campaign=Badge_Grade_Dashboard) +[![Go Reference](https://pkg.go.dev/badge/refs/pman.svg)](https://pkg.go.dev/refs/pman) ## Development diff --git a/go.sum b/go.sum index 4d9e821..f6a8994 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,7 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -35,6 +36,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -67,6 +69,7 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -99,14 +102,17 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= @@ -137,6 +143,7 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -158,7 +165,9 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -178,6 +187,7 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -302,6 +312,7 @@ google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 50f23cb..56e53fc 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -14,12 +14,14 @@ var ( Hostname string Port string + KeepAlive bool ) // RootCmd returns a configured root command. func RootCmd(cfg *config.Config) *cobra.Command { rootCmd.PersistentFlags().StringVarP(&Hostname, "hostname", "n", "", "host with a running ocis runtime.") rootCmd.PersistentFlags().StringVarP(&Port, "port", "p", "", "port to send messages to the rpc ocis runtime.") + rootCmd.PersistentFlags().BoolVarP(&KeepAlive, "keep-alive", "k", false, "restart supervised processes that abruptly die.") viper.BindPFlag("hostname", rootCmd.PersistentFlags().Lookup("hostname")) viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port")) diff --git a/pkg/config/config.go b/pkg/config/config.go index a7ce1ab..ca908bd 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,17 +4,22 @@ package config type Config struct { Hostname string Port string + File string + KeepAlive bool } var ( defaultHostname = "localhost" defaultPort = "10666" + defaultFile = "/var/tmp/.pman" ) // NewConfig returns a new config with a set of defaults. func NewConfig() *Config { return &Config{ - Hostname: defaultHostname, - Port: defaultPort, + Hostname: defaultHostname, + Port: defaultPort, + File: defaultFile, + KeepAlive: false, } } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 4cc4a3f..a43282c 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -4,12 +4,12 @@ import ( "encoding/json" "fmt" "io/ioutil" - golog "log" "os" "os/exec" "sort" "strconv" "strings" + "sync" "github.com/refs/pman/pkg/log" "github.com/refs/pman/pkg/process" @@ -21,37 +21,45 @@ import ( // Controller writes the current managed processes onto a file, or any ReadWrite. type Controller struct { + m *sync.RWMutex + options Options + log zerolog.Logger // File refers to the Controller database, where we keep the controller's status. It formats as json. File string - // Bin is the ocis single binary name. Bin string - // BinPath is the ocis single binary path withing the host machine. // The Controller needs to know the binary location in order to spawn new extensions. BinPath string - - log zerolog.Logger + // Terminated is a bidirectional channel that tallows communication from Watcher <-> Controller. Writes to this + // channel will attempt to restart the crashed process. + Terminated chan process.ProcEntry + // restarted keeps an account of how many times a process has been restarted. + restarted map[string]int } var ( - defaultFile = "/var/tmp/.pman" + once = sync.Once{} ) // NewController initializes a new controller. func NewController(o ...Option) Controller { + opts := &Options{} + + for _, f := range o { + f(opts) + } + c := Controller{ Bin: "ocis", - File: defaultFile, + File: opts.File, + Terminated: make(chan process.ProcEntry), log: log.NewLogger( log.WithPretty(true), ), - } - - opts := &Options{} - - for _, f := range o { - f(opts) + options: *opts, + restarted: map[string]int{}, + m: &sync.RWMutex{}, } if opts.Bin != "" { @@ -61,53 +69,83 @@ func NewController(o ...Option) Controller { // Get binary location from $PATH lookup. If not present, it uses arg[0] as entry point. path, err := exec.LookPath(c.Bin) if err != nil { - golog.Print("oCIS binary not present on `$PATH`") + c.log.Debug().Msg("OCIS binary not present in PATH, using Args[0]") path = os.Args[0] } c.BinPath = path - if _, err := os.Stat(defaultFile); err != nil { - c.log.Info().Str("package", "watcher").Msgf("db file doesn't exist, creating one with contents: `{}`") - ioutil.WriteFile(defaultFile, []byte("{}"), 0644) + if _, err := os.Stat(opts.File); err != nil { + c.log.Debug().Str("package", "controller").Msgf("setting up db") + ioutil.WriteFile(opts.File, []byte("{}"), 0644) } return c } -// Write a new entry to File. -func (c *Controller) Write(pe process.ProcEntry) error { - fd, err := ioutil.ReadFile(c.File) +// write a new entry to File. +func (c *Controller) write(pe process.ProcEntry) error { + c.m.RLock() + defer c.m.RUnlock() + + entries, err := loadDB(c.File) if err != nil { return err } - entries := make(map[string]int) - json.Unmarshal(fd, &entries) - entries[pe.Extension] = pe.Pid return c.writeEntries(entries) } // Start and watches a process. func (c *Controller) Start(pe process.ProcEntry) error { + // TODO add support for the same process running on different ports. a.k.a db entries as []string. + var err error + var pid int + + if pid, err = c.storedPID(pe.Extension); pid != 0 { + c.log.Debug().Msg(fmt.Sprintf("extension already running: %s", pe.Extension)) + return nil + } + if err != nil { + return err + } + w := watcher.NewWatcher() if err := pe.Start(c.BinPath); err != nil { return err } - if err := c.Write(pe); err != nil { + if err := c.write(pe); err != nil { return err } - w.Follow(pe) + w.Follow(pe, c.Terminated, c.options.Restart) + + once.Do(func() { + go detach(c) + }) return nil } +// detach will try to restart processes on failures. +func detach(c *Controller) { + func(c *Controller) { + for { + select { + case proc := <- c.Terminated: + if err := c.Start(proc); err != nil { + c.log.Err(err) + } + } + } + }(c) +} + // Kill a managed process. func (c *Controller) Kill(ext *string) error { - pid, err := c.pidFromName(*ext) + pid, err := c.storedPID(*ext) if err != nil { return err } @@ -116,19 +154,21 @@ func (c *Controller) Kill(ext *string) error { return err } + if err := c.delete(*ext); err != nil { + return err + } c.log.Info().Str("package", "watcher").Msgf("terminating %v", *ext) return p.Kill() } // Shutdown a running runtime. func (c *Controller) Shutdown(ch chan struct{}) error { - fd, err := ioutil.ReadFile(c.File) + c.m.Lock() + entries, err := loadDB(c.File) if err != nil { return err } - - entries := make(map[string]int) - json.Unmarshal(fd, &entries) + c.m.Unlock() for cmd, pid := range entries { c.log.Info().Str("package", "watcher").Msgf("gracefully terminating %v", cmd) @@ -149,15 +189,14 @@ func (c *Controller) Shutdown(ch chan struct{}) error { func (c *Controller) List() string { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) - table.SetHeader([]string{"Extension", "PID"}) - fd, err := ioutil.ReadFile(c.File) + + c.m.Lock() + entries, err := loadDB(c.File) if err != nil { - c.log.Fatal().Err(err) + c.log.Err(err).Msg(fmt.Sprintf("error loading file: %s", c.File)) } - - entries := make(map[string]int) - json.Unmarshal(fd, &entries) + c.m.Unlock() keys := make([]string, 0, len(entries)) for k := range entries { @@ -176,31 +215,53 @@ func (c *Controller) List() string { // Reset clears the db file. func (c *Controller) Reset() error { - return ioutil.WriteFile(defaultFile, []byte("{}"), 0644) + c.m.RLock() + defer c.m.RUnlock() + return ioutil.WriteFile(c.File, []byte("{}"), 0644) } -// pidFromName reads from controller's db for the extension name, and returns it's pid for the running process. -func (c *Controller) pidFromName(name string) (int, error) { - fd, err := ioutil.ReadFile(c.File) +// delete removes a managed process from db. +func (c *Controller) delete(name string) error { + c.m.Lock() + entries, err := loadDB(c.File) if err != nil { - return 0, err + return err } + c.m.Unlock() - entries := make(map[string]int) - json.Unmarshal(fd, &entries) - - pid, ok := entries[name] + _, ok := entries[name] if !ok { - return 0, fmt.Errorf("pid for extension `%v` not found", name) + return fmt.Errorf("pid not found for extension: %v", name) } delete(entries, name) - c.writeEntries(entries) + + c.m.RLock() + defer c.m.RUnlock() + return c.writeEntries(entries) +} + +// storedPID reads from controller's db for the extension name, and returns it's pid for the running process. +func (c *Controller) storedPID(name string) (int, error) { + c.m.Lock() + entries, err := loadDB(c.File) + if err != nil { + return 0, err + } + c.m.Unlock() + + pid, ok := entries[name] + if !ok { + return 0, nil + } return pid, nil } func (c *Controller) writeEntries(e map[string]int) error { + c.m.RLock() + defer c.m.RUnlock() + bytes, err := json.Marshal(e) if err != nil { return err @@ -208,3 +269,18 @@ func (c *Controller) writeEntries(e map[string]int) error { return ioutil.WriteFile(c.File, bytes, 0644) } + +// loadDB loads pman db file from disk. +func loadDB(file string) (map[string]int, error) { + contents, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + entries := make(map[string]int) + if err := json.Unmarshal(contents, &entries); err != nil { + return nil, err + } + + return entries, nil +} \ No newline at end of file diff --git a/pkg/controller/option.go b/pkg/controller/option.go index 604e1db..3dbd5b2 100644 --- a/pkg/controller/option.go +++ b/pkg/controller/option.go @@ -4,6 +4,8 @@ package controller type Options struct { Bin string File string + Restart bool + Grace int } // Option represents an option. @@ -27,3 +29,17 @@ func WithFile(file string) Option { o.File = file } } + +// WithRestart sets restart, which control whether a controller restart killed processes. +func WithRestart(r bool) Option { + return func(o *Options) { + o.Restart = r + } +} + +// WithGrace sets restart, which control whether a controller restart killed processes. +func WithGrace(g int) Option { + return func(o *Options) { + o.Grace = g + } +} diff --git a/pkg/process/process.go b/pkg/process/process.go index 669515e..36766fb 100644 --- a/pkg/process/process.go +++ b/pkg/process/process.go @@ -44,7 +44,6 @@ func (e *ProcEntry) Start(binPath string) error { if err != nil { return err } - e.Pid = p.Pid return nil diff --git a/pkg/service/service.go b/pkg/service/service.go index 4af99bc..6e495a4 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -1,6 +1,7 @@ package service import ( + "github.com/spf13/viper" "net" "net/http" "net/rpc" @@ -8,6 +9,7 @@ import ( "os/signal" "syscall" + "github.com/refs/pman/pkg/config" "github.com/refs/pman/pkg/controller" "github.com/refs/pman/pkg/log" "github.com/refs/pman/pkg/process" @@ -17,20 +19,47 @@ import ( // Service represents a RPC service. // The controller manager the service's state. When an action on a service is required, // this will read the PID from the DB file for the given extensions and act upon its PID. +// This package would act as a root command of sorts, since PMAN having 2 operational modes, as a +// cli tool and a library. type Service struct { Controller controller.Controller Log zerolog.Logger } +// loadFromEnv would set cmd global variables. This is a workaround spf13/viper since pman used as a library does not +// parse flags. +func loadFromEnv(cfg *config.Config) { + viper.AutomaticEnv() + + _ = viper.BindEnv("keep-alive", "RUNTIME_KEEP_ALIVE") + _ = viper.BindEnv("file", "RUNTIME_DB_FILE") + + cfg.KeepAlive = viper.GetBool("keep-alive") + + if viper.GetString("file") != "" { + cfg.File = viper.GetString("file") + } +} + // NewService returns a configured service with a controller and a default logger. +// When used as a library, flags are not parsed, and in order to avoid introducing a global state with init functions +// calls are done explicitly to loadFromEnv(*config.Config).\ +// Since this is the public constructor, options need to be added, at the moment only logging options +// are supported in order to match the running OwnCloud services structured log. func NewService(options ...log.Option) *Service { + cfg := config.NewConfig() + loadFromEnv(cfg) + return &Service{ - Controller: controller.NewController(), + Controller: controller.NewController( + controller.WithRestart(cfg.KeepAlive), + controller.WithFile(cfg.File), + ), Log: log.NewLogger(options...), } } -// Start a process +// Start indicates the Service Controller to start a new supervised service as an OS thread. func (s *Service) Start(args process.ProcEntry, reply *int) error { if err := s.Controller.Start(args); err != nil { *reply = 1 @@ -41,13 +70,14 @@ func (s *Service) Start(args process.ProcEntry, reply *int) error { return nil } -// List running processes for the controller. +// List running processes for the Service Controller. func (s *Service) List(args struct{}, reply *string) error { *reply = s.Controller.List() return nil } -// Kill a process +// Kill a supervised process by subcommand name. +// TODO this API is rather simple and prone to failure. Terminate a process by PID MUST be allowed. func (s *Service) Kill(args *string, reply *int) error { if err := s.Controller.Kill(args); err != nil { *reply = 1 diff --git a/pkg/watcher/watcher.go b/pkg/watcher/watcher.go index 3c8f000..d5b1fd1 100644 --- a/pkg/watcher/watcher.go +++ b/pkg/watcher/watcher.go @@ -21,8 +21,8 @@ func NewWatcher() Watcher { } } -// Follow a process until it dies. -func (w *Watcher) Follow(pe process.ProcEntry) { +// Follow a process until it dies. If restart is enabled, a new fork of the original process will be automatically spawned. +func (w *Watcher) Follow(pe process.ProcEntry, followerChan chan process.ProcEntry, restart bool) { state := make(chan *os.ProcessState, 1) w.log.Debug().Str("package", "watcher").Msgf("watching %v", pe.Extension) @@ -39,6 +39,9 @@ func (w *Watcher) Follow(pe process.ProcEntry) { select { case status := <-state: w.log.Info().Str("package", "watcher").Msgf("%v exited with code: %v", pe.Extension, status.ExitCode()) + if restart { + followerChan <- pe + } } }() }