Files
opencloud/ocis/pkg/runtime/controller/controller.go
2021-01-28 00:32:47 +00:00

169 lines
3.5 KiB
Go

package controller
import (
"fmt"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/owncloud/ocis/ocis/pkg/runtime/config"
"github.com/owncloud/ocis/ocis/pkg/runtime/process"
"github.com/owncloud/ocis/ocis/pkg/runtime/storage"
"github.com/owncloud/ocis/ocis/pkg/runtime/watcher"
"github.com/rs/zerolog"
"github.com/olekukonko/tablewriter"
)
// Controller supervises processes.
type Controller struct {
m *sync.RWMutex
options Options
log zerolog.Logger
Config *config.Config
Store storage.Storage
// 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
// Terminated facilitates communication from Watcher <-> Controller. Writes to this
// channel WILL always attempt to restart the crashed process.
Terminated chan process.ProcEntry
}
var (
once = sync.Once{}
)
// NewController initializes a new controller.
func NewController(o ...Option) Controller {
opts := &Options{}
for _, f := range o {
f(opts)
}
c := Controller{
m: &sync.RWMutex{},
options: *opts,
log: *opts.Log,
Bin: "ocis",
Terminated: make(chan process.ProcEntry),
Store: storage.NewMapStorage(),
Config: opts.Config,
}
if opts.Bin != "" {
c.Bin = opts.Bin
}
// 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 {
c.log.Debug().Msg("oCIS binary not present in PATH, using Args[0]")
path = os.Args[0]
}
c.BinPath = path
return c
}
// Start and watches a process.
func (c *Controller) Start(pe process.ProcEntry) error {
if pid := c.Store.Load(pe.Extension); pid != 0 {
c.log.Debug().Msg(fmt.Sprintf("extension already running: %s", pe.Extension))
return nil
}
w := watcher.NewWatcher()
if err := pe.Start(c.BinPath); err != nil {
return err
}
// store the spawned child process PID.
if err := c.Store.Store(pe); err != nil {
return err
}
w.Follow(pe, c.Terminated, c.options.Config.KeepAlive)
once.Do(func() {
j := janitor{
time.Second,
c.Store,
}
go j.run()
go detach(c)
})
return nil
}
// Kill a managed process.
// Should a process managed by the runtime be allowed to be killed if the runtime is configured not to?
func (c *Controller) Kill(pe process.ProcEntry) error {
// load stored PID
pid := c.Store.Load(pe.Extension)
// find process in host by PID
p, err := os.FindProcess(pid)
if err != nil {
return err
}
if err := c.Store.Delete(pe); err != nil {
return err
}
c.log.Info().Str("package", "watcher").Msgf("terminating %v", pe.Extension)
// terminate child process
return p.Kill()
}
// Shutdown a running runtime.
func (c *Controller) Shutdown(ch chan struct{}) error {
entries := c.Store.LoadAll()
for cmd, pid := range entries {
c.log.Info().Str("package", "watcher").Msgf("gracefully terminating %v", cmd)
p, _ := os.FindProcess(pid)
if err := p.Kill(); err != nil {
return err
}
}
ch <- struct{}{}
return nil
}
// List managed processes.
func (c *Controller) List() string {
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
table.SetHeader([]string{"Extension", "PID"})
entries := c.Store.LoadAll()
keys := make([]string, 0, len(entries))
for k := range entries {
keys = append(keys, k)
}
sort.Strings(keys)
for _, v := range keys {
table.Append([]string{v, strconv.Itoa(entries[v])})
}
table.Render()
return tableString.String()
}