wip: daemon

This commit is contained in:
Suyono 2023-09-01 12:29:31 +10:00
parent eac78ec322
commit 91147f073f
8 changed files with 332 additions and 30 deletions

View File

@ -16,8 +16,8 @@ const (
defaultPath = "/etc/wingmate/"
defaultName = "config"
serviceKey = "service"
cronKey = "cron"
ServiceKey = "service"
CronKey = "cron"
)
var (

View File

@ -1,39 +1,92 @@
package config
import (
"gitea.suyono.dev/suyono/wingmate/files/testconfig"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os"
"strings"
"testing"
)
type testReadArgs struct {
cmd *cobra.Command
args []string
}
type testRead struct {
name string
env map[string]string
args testReadArgs
wantErr bool
pre func(t *testing.T, tc *testRead)
post func(t *testing.T, tc *testRead)
}
func TestRead(t *testing.T) {
type args struct {
cmd *cobra.Command
args []string
}
tests := []struct {
name string
env map[string]string
args args
wantErr bool
}{
tests := []testRead{
{
name: "env",
env: map[string]string{
strings.ToUpper(envPrefix + "_" + configPathKey): "/path/to/config",
strings.ToUpper(envPrefix + "_" + configSearchPathKey): "/path/one,/path/two",
},
args: args{
args: testReadArgs{
nil,
[]string{},
},
wantErr: true,
},
{
name: "env exist",
args: testReadArgs{
nil,
[]string{},
},
wantErr: false,
pre: func(t *testing.T, tc *testRead) {
var (
f *os.File
err error
fname 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)
}
tc.env = map[string]string{
strings.ToUpper(envPrefix + "_" + configPathKey): fname,
}
tc.post = func(t *testing.T, tc *testRead) {
_ = os.Remove(fname)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var err error
if tt.pre != nil {
tt.pre(t, &tt)
}
if tt.post != nil {
defer tt.post(t, &tt)
}
for k, v := range tt.env {
if err = os.Setenv(k, v); err != nil {
t.Fatal("failed", err)
@ -57,3 +110,82 @@ func TestRead(t *testing.T) {
})
}
}
func TestGet(t *testing.T) {
type args struct {
cmd *cobra.Command
args []string
}
tests := []testRead{
{
name: "env exist",
args: testReadArgs{
nil,
[]string{},
},
wantErr: false,
pre: func(t *testing.T, tc *testRead) {
var (
f *os.File
err error
fname 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)
}
tc.env = map[string]string{
strings.ToUpper(envPrefix + "_" + configPathKey): fname,
}
tc.post = func(t *testing.T, tc *testRead) {
_ = os.Remove(fname)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var err error
if tt.pre != nil {
tt.pre(t, &tt)
}
if tt.post != nil {
defer tt.post(t, &tt)
}
for k, v := range tt.env {
if err = os.Setenv(k, v); err != nil {
t.Fatal("failed", err)
}
}
defer func() {
for k := range tt.env {
if err = os.Unsetenv(k); err != nil {
t.Fatal("failed", err)
}
}
}()
if err = Read(tt.args.cmd, tt.args.args); err != nil {
t.Fatal("fail to read config:", err)
}
t.Log(viper.AllKeys())
m := viper.GetStringMap(ServiceKey)
for s := range m {
t.Log(s)
t.Log(viper.GetStringMap(ServiceKey + "." + s))
}
})
}
}

View File

@ -1,9 +1,71 @@
package daemon
import "github.com/spf13/cobra"
import (
"gitea.suyono.dev/suyono/wingmate/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os/exec"
)
type InstanceType int
type WingMateDescriptor interface {
String() string
Name() string
InstanceName() string
InstanceType() InstanceType
Error() string // so the descriptor can be wrapped inside an error
}
type process struct {
cmd *exec.Cmd
descriptor WingMateDescriptor
}
type daemon struct {
running map[string]process
available map[string]process
services map[string]WingMateDescriptor
cron map[string]WingMateDescriptor
}
const (
Service InstanceType = iota
Cron
)
func (t InstanceType) String() string {
switch t {
case Service:
return config.ServiceKey
case Cron:
return config.CronKey
default:
return "[unknown instance type]"
}
}
func Start(cmd *cobra.Command, args []string) error {
var (
err error
)
_, _ = cmd, args // prevent warnings for unused arguments
d := &daemon{
running: make(map[string]process),
available: make(map[string]process),
services: make(map[string]WingMateDescriptor),
cron: make(map[string]WingMateDescriptor),
}
if err = d.buildServiceMap(); err != nil {
return err
}
return nil
}
func (d *daemon) buildServiceMap() error {
for s := range viper.GetStringMap(Service.String()) {
_ = s //TODO: resume work
}
return nil
}

View File

@ -1,23 +1,117 @@
package daemon
import (
"bufio"
"fmt"
"gitea.suyono.dev/suyono/wingmate/debugframes"
"io"
"os"
"os/exec"
)
type ProcessConfig interface {
Descriptor() WingMateDescriptor
// Name returns the binary name to be executed, to be passed in to exec.Command
Name() string
// Args returns the arguments for the process, to be passed in to exec.Command
Args() []string
Env() map[string]string
Dir() string
WorkingDir() string
LogChannel() chan<- any
ControlChannel() chan<- any
}
func StartProcess(config ProcessConfig) error {
cmd := exec.Command(config.Name(), config.Args()...)
for k, v := range config.Env() {
type StreamID int
type ProcessLogEntry struct {
ID WingMateDescriptor
LogEntry string
}
const (
Stdout StreamID = iota
Stderr
)
func (s StreamID) String() string {
switch s {
case Stdout:
return "stdout"
case Stderr:
return "stderr"
default:
return "[unknown stream]"
}
}
func StartProcess(config ProcessConfig) (*exec.Cmd, error) {
var (
err error
cmd *exec.Cmd
k, v string
stdoutPipe, stderrPipe io.ReadCloser
)
cmd = exec.Command(config.Name(), 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.Dir()
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())
}
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
if err = cmd.Start(); err != nil {
return nil, fmt.Errorf("starting process %w: %w %w", config.Descriptor(), err, debugframes.GetTraces())
}
go serviceLogReader(config, stdoutPipe, Stdout)
go serviceLogReader(config, stderrPipe, Stderr)
go serviceWaiter(config, cmd)
return cmd, nil
}
func serviceLogReader(config ProcessConfig, stream io.Reader, id StreamID) {
out := config.LogChannel()
defer func() {
if o := recover(); o != nil {
out <- o
//TODO: include stack trace, make the return object an error
}
}()
scanner := bufio.NewScanner(stream)
for scanner.Scan() {
out <- ProcessLogEntry{
ID: 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())
}
}
func serviceWaiter(config ProcessConfig, cmd *exec.Cmd) {
ctrl := config.ControlChannel()
defer func() {
if o := recover(); o != nil {
ctrl <- o
//TODO: include stack trace
}
}()
if err := cmd.Wait(); err != nil {
ctrl <- fmt.Errorf("wait error on service %w: %w", config.Descriptor(), err)
} else {
ctrl <- config.Descriptor()
}
}

View File

@ -1,4 +1,12 @@
package daemon
func DetectDeadChildProcess() {
import "golang.org/x/sys/unix"
const (
anyChildProcess = -1
)
func WaitChildProcess() (int, error) {
var ws unix.WaitStatus
return unix.Wait4(anyChildProcess, &ws, 0, nil)
}

View File

@ -15,7 +15,6 @@ func TestWait(t *testing.T) {
err error
pid int
testpid int
ws unix.WaitStatus
)
cmd := exec.Command("sleep", "10")
@ -37,22 +36,14 @@ func TestWait(t *testing.T) {
}
}()
testpid, err = unix.Wait4(-1, &ws, 0, nil)
testpid, err = WaitChildProcess()
if err != nil {
t.Fatal("wait4 error:", err)
}
if ws.Signaled() {
t.Log("the child process got signal:", ws.Signal())
}
if ws.Exited() {
t.Log("the child process exit with status:", ws.ExitStatus())
}
// if err = cmd.Wait(); err != nil {
// t.Fatal("is this expected?", err)
// }
//if err = cmd.Wait(); err != nil {
// t.Fatal("is this expected?", err)
//}
assert.Equal(t, pid, testpid)
}

9
files/testconfig/one.go Normal file
View File

@ -0,0 +1,9 @@
package testconfig
var One = `service:
one:
command: "mycommand -o output"
two:
command: ["cmd", "now"]
workdir: /
`

6
files/testconfig/one.yml Normal file
View File

@ -0,0 +1,6 @@
service:
one:
command: "mycommand -o output"
two:
command: ["cmd", "now"]
workdir: /