This commit is contained in:
2023-09-03 12:11:35 +10:00
parent 91147f073f
commit badd2e3543
10 changed files with 343 additions and 49 deletions

View File

@@ -1,10 +1,13 @@
package daemon
import (
"fmt"
"gitea.suyono.dev/suyono/wingmate/config"
"gitea.suyono.dev/suyono/wingmate/debugframes"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os/exec"
"runtime/debug"
)
type InstanceType int
@@ -23,10 +26,11 @@ type process struct {
}
type daemon struct {
running map[string]process
available map[string]process
services map[string]WingMateDescriptor
cron map[string]WingMateDescriptor
running map[string]process
services map[string]*service
cron map[string]WingMateDescriptor
logChannel chan<- any
errorChannel chan<- error
}
const (
@@ -45,27 +49,61 @@ func (t InstanceType) String() string {
}
}
func Start(cmd *cobra.Command, args []string) error {
var (
err error
)
func Start(cmd *cobra.Command, args []string) (err error) {
defer func() {
if o := recover(); o != nil {
if err != nil {
err = fmt.Errorf("panic: %v %w and error: %w", o, debugframes.PanicTrace(debug.Stack()), err)
} else {
err = fmt.Errorf("panic: %v %w", o, debugframes.PanicTrace(debug.Stack()))
}
}
}()
_, _ = cmd, args // prevent warnings for unused arguments
_, err = start()
return
}
func start() (*daemon, error) {
var (
err error
k string
svc *service
cmd *exec.Cmd
)
d := &daemon{
running: make(map[string]process),
available: make(map[string]process),
services: make(map[string]WingMateDescriptor),
cron: make(map[string]WingMateDescriptor),
running: make(map[string]process),
services: make(map[string]*service),
cron: make(map[string]WingMateDescriptor),
logChannel: make(chan<- any, 16),
errorChannel: make(chan<- error, 4),
}
if err = d.buildServiceMap(); err != nil {
return err
return nil, err
}
return nil
for k, svc = range d.services {
if cmd, err = StartProcess(svc); err != nil {
return nil, err //TODO: this is not supposed to return, log and start the next service
}
d.running[k] = process{
cmd: cmd,
descriptor: svc,
}
}
//TODO: create loop to receive and process log
//TODO: create loop to receive error
//TODO: create signal handler
return d, nil
}
func (d *daemon) buildServiceMap() error {
for s := range viper.GetStringMap(Service.String()) {
_ = s //TODO: resume work
d.services[s] = newService(s, d.errorChannel, d.logChannel)
}
return nil
}

View File

@@ -7,27 +7,28 @@ import (
"io"
"os"
"os/exec"
"runtime/debug"
)
type ProcessConfig interface {
Descriptor() WingMateDescriptor
// Name returns the binary name to be executed, to be passed in to exec.Command
Name() string
// ExecutableName returns the executable/binary name to be executed, to be passed in to exec.Command
ExecutableName() string
// Args returns the arguments for the process, to be passed in to exec.Command
Args() []string
Env() map[string]string
WorkingDir() string
LogChannel() chan<- any
ControlChannel() chan<- any
ErrorChannel() chan<- error
}
type StreamID int
type ProcessLogEntry struct {
ID WingMateDescriptor
LogEntry string
Descriptor WingMateDescriptor
LogEntry string
}
const (
@@ -53,17 +54,19 @@ func StartProcess(config ProcessConfig) (*exec.Cmd, error) {
k, v string
stdoutPipe, stderrPipe io.ReadCloser
)
cmd = exec.Command(config.Name(), config.Args()...)
cmd = exec.Command(config.ExecutableName(), config.Args()...)
cmd.Env = os.Environ()
for k, v = range config.Env() {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}
cmd.Dir = config.WorkingDir()
if stdoutPipe, err = cmd.StdoutPipe(); err != nil {
return nil, fmt.Errorf("set up stdout pipe on process %w: %w %w", config.Descriptor(), err, debugframes.GetTraces())
return nil, fmt.Errorf("set up stdout pipe on process %w: %w %w",
config.Descriptor(), err, debugframes.GetTraces())
}
if stderrPipe, err = cmd.StderrPipe(); err != nil {
return nil, fmt.Errorf("set up stderr pipe on process %w: %w %w", config.Descriptor(), err, debugframes.GetTraces())
return nil, fmt.Errorf("set up stderr pipe on process %w: %w %w",
config.Descriptor(), err, debugframes.GetTraces())
}
if err = cmd.Start(); err != nil {
@@ -80,38 +83,41 @@ func StartProcess(config ProcessConfig) (*exec.Cmd, error) {
func serviceLogReader(config ProcessConfig, stream io.Reader, id StreamID) {
out := config.LogChannel()
eChan := config.ErrorChannel()
defer func() {
if o := recover(); o != nil {
out <- o
//TODO: include stack trace, make the return object an error
eChan <- fmt.Errorf("log reader %s of %w panic: %v %w",
id.String(), config.Descriptor(), o, debugframes.PanicTrace(debug.Stack()))
} else {
eChan <- fmt.Errorf("log reader %s of %w returned", id.String(), config.Descriptor())
}
}()
scanner := bufio.NewScanner(stream)
for scanner.Scan() {
out <- ProcessLogEntry{
ID: config.Descriptor(),
LogEntry: scanner.Text(),
Descriptor: config.Descriptor(),
LogEntry: scanner.Text(),
}
}
if err := scanner.Err(); err != nil {
out <- fmt.Errorf("read stream from %w: %w %w", config.Descriptor(), err, debugframes.GetTraces())
eChan <- fmt.Errorf("read %s from %w: %w %w",
id.String(), config.Descriptor(), err, debugframes.GetTraces())
}
}
func serviceWaiter(config ProcessConfig, cmd *exec.Cmd) {
ctrl := config.ControlChannel()
eChan := config.ErrorChannel()
defer func() {
if o := recover(); o != nil {
ctrl <- o
//TODO: include stack trace
eChan <- fmt.Errorf("%w panic: %v %w", config.Descriptor(), o, debugframes.PanicTrace(debug.Stack()))
}
}()
if err := cmd.Wait(); err != nil {
ctrl <- fmt.Errorf("wait error on service %w: %w", config.Descriptor(), err)
eChan <- fmt.Errorf("wait error on service %w: %w", config.Descriptor(), err)
} else {
ctrl <- config.Descriptor()
eChan <- config.Descriptor()
}
}

86
daemon/service.go Normal file
View File

@@ -0,0 +1,86 @@
package daemon
import (
"fmt"
"gitea.suyono.dev/suyono/wingmate/config"
"github.com/spf13/viper"
)
type service struct {
name string
logChannel chan<- any
errChannel chan<- error
}
func newService(name string, error chan<- error, logging chan<- any) *service {
return &service{
name: name,
logChannel: logging,
errChannel: error,
}
}
//TODO: review back the implementation of service as WingMateDescriptor
func (s *service) String() string {
return s.name
}
func (s *service) Name() string {
return s.name
}
func (s *service) InstanceName() string {
return s.name
}
func (s *service) InstanceType() InstanceType {
return Service
}
func (s *service) Error() string {
return s.name
}
func (s *service) parseCommand() []string {
key := fmt.Sprintf("%s.%s.%s", Service.String(), s.name, config.CommandKey)
if !viper.IsSet(key) {
panic(fmt.Errorf("parse command: key %s is not set", key))
}
result := viper.GetStringSlice(key)
if len(result) < 1 {
panic(fmt.Errorf("parse command: zero command length"))
}
return result
}
func (s *service) ExecutableName() string {
return s.parseCommand()[0]
}
func (s *service) Args() []string {
return s.parseCommand()[1:]
}
func (s *service) Descriptor() WingMateDescriptor {
return s
}
func (s *service) Env() map[string]string {
//TODO: stub implementation; FIX!
return make(map[string]string)
}
func (s *service) WorkingDir() string {
//TODO: stub implementation; FIX!
return ""
}
func (s *service) LogChannel() chan<- any {
return s.logChannel
}
func (s *service) ErrorChannel() chan<- error {
return s.errChannel
}

82
daemon/service_test.go Normal file
View File

@@ -0,0 +1,82 @@
package daemon
import (
"gitea.suyono.dev/suyono/wingmate/config"
"gitea.suyono.dev/suyono/wingmate/files/testconfig"
"github.com/spf13/viper"
"os"
"strings"
"testing"
)
type parseCommandTestCase struct {
name string
pre func(t *testing.T, tt *parseCommandTestCase)
post func(t *testing.T, tt *parseCommandTestCase)
}
func TestParseCommand(t *testing.T) {
tests := []parseCommandTestCase{
{
name: "initial",
pre: func(t *testing.T, tt *parseCommandTestCase) {
var (
f *os.File
err error
fname, key string
)
if f, err = os.CreateTemp("", "config-*.yml"); err != nil {
t.Fatal("create temp:", err)
}
fname = f.Name()
if _, err = f.WriteString(testconfig.One); err != nil {
t.Fatal("writing temp:", err)
}
if err = f.Close(); err != nil {
t.Fatal("closing temp:", err)
}
key = strings.ToUpper(config.EnvPrefix + "_" + config.PathKey)
if err = os.Setenv(key, fname); err != nil {
t.Fatal("set up env failed", err)
}
tt.post = func(t *testing.T, tt *parseCommandTestCase) {
_ = os.Unsetenv(key)
_ = os.Remove(fname)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
err error
d *daemon
s *service
)
if tt.pre != nil {
tt.pre(t, &tt)
}
if tt.post != nil {
defer tt.post(t, &tt)
}
if err = config.Read(nil, []string{}); err != nil {
t.Fatal("reading config", err)
}
if d, err = start(); err != nil {
t.Fatal("starting daemon", err)
}
for _, s = range d.services {
s.parseCommand()
}
x := viper.GetStringSlice("non.existent")
t.Log(x)
})
}
}