wip: daemon
This commit is contained in:
parent
eac78ec322
commit
91147f073f
@ -16,8 +16,8 @@ const (
|
|||||||
defaultPath = "/etc/wingmate/"
|
defaultPath = "/etc/wingmate/"
|
||||||
defaultName = "config"
|
defaultName = "config"
|
||||||
|
|
||||||
serviceKey = "service"
|
ServiceKey = "service"
|
||||||
cronKey = "cron"
|
CronKey = "cron"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@ -1,39 +1,92 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/files/testconfig"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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) {
|
func TestRead(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
cmd *cobra.Command
|
cmd *cobra.Command
|
||||||
args []string
|
args []string
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []testRead{
|
||||||
name string
|
|
||||||
env map[string]string
|
|
||||||
args args
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
{
|
||||||
name: "env",
|
name: "env",
|
||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
strings.ToUpper(envPrefix + "_" + configPathKey): "/path/to/config",
|
strings.ToUpper(envPrefix + "_" + configPathKey): "/path/to/config",
|
||||||
strings.ToUpper(envPrefix + "_" + configSearchPathKey): "/path/one,/path/two",
|
strings.ToUpper(envPrefix + "_" + configSearchPathKey): "/path/one,/path/two",
|
||||||
},
|
},
|
||||||
args: args{
|
args: testReadArgs{
|
||||||
nil,
|
nil,
|
||||||
[]string{},
|
[]string{},
|
||||||
},
|
},
|
||||||
wantErr: true,
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
var err error
|
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 {
|
for k, v := range tt.env {
|
||||||
if err = os.Setenv(k, v); err != nil {
|
if err = os.Setenv(k, v); err != nil {
|
||||||
t.Fatal("failed", err)
|
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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,71 @@
|
|||||||
package daemon
|
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 {
|
func Start(cmd *cobra.Command, args []string) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
)
|
||||||
_, _ = cmd, args // prevent warnings for unused arguments
|
_, _ = 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,117 @@
|
|||||||
package daemon
|
package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/debugframes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProcessConfig interface {
|
type ProcessConfig interface {
|
||||||
|
Descriptor() WingMateDescriptor
|
||||||
|
|
||||||
|
// Name returns the binary name to be executed, to be passed in to exec.Command
|
||||||
Name() string
|
Name() string
|
||||||
|
|
||||||
|
// Args returns the arguments for the process, to be passed in to exec.Command
|
||||||
Args() []string
|
Args() []string
|
||||||
Env() map[string]string
|
Env() map[string]string
|
||||||
Dir() string
|
WorkingDir() string
|
||||||
|
LogChannel() chan<- any
|
||||||
|
ControlChannel() chan<- any
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartProcess(config ProcessConfig) error {
|
type StreamID int
|
||||||
cmd := exec.Command(config.Name(), config.Args()...)
|
|
||||||
for k, v := range config.Env() {
|
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.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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,12 @@
|
|||||||
package daemon
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,6 @@ func TestWait(t *testing.T) {
|
|||||||
err error
|
err error
|
||||||
pid int
|
pid int
|
||||||
testpid int
|
testpid int
|
||||||
ws unix.WaitStatus
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd := exec.Command("sleep", "10")
|
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 {
|
if err != nil {
|
||||||
t.Fatal("wait4 error:", err)
|
t.Fatal("wait4 error:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ws.Signaled() {
|
//if err = cmd.Wait(); err != nil {
|
||||||
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)
|
// t.Fatal("is this expected?", err)
|
||||||
// }
|
//}
|
||||||
|
|
||||||
assert.Equal(t, pid, testpid)
|
assert.Equal(t, pid, testpid)
|
||||||
}
|
}
|
||||||
|
|||||||
9
files/testconfig/one.go
Normal file
9
files/testconfig/one.go
Normal 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
6
files/testconfig/one.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
service:
|
||||||
|
one:
|
||||||
|
command: "mycommand -o output"
|
||||||
|
two:
|
||||||
|
command: ["cmd", "now"]
|
||||||
|
workdir: /
|
||||||
Loading…
x
Reference in New Issue
Block a user